#!/usr/bin/env python3
"""
LinkedIn Publisher — AgileAdapt / ReceptionistAI
Playwright-based LinkedIn posting for B2B content.

Schedule: Monday, Wednesday, Friday — 8:00am AEST
Target: Business owners, operations managers, tradies, agency partners

Usage:
  python linkedin_publisher.py --post-now --post-file path/to/post.md
  python linkedin_publisher.py --schedule                  # posts at next Mon/Wed/Fri 8am AEST
  python linkedin_publisher.py --dry-run --topic "AI ROI"  # preview generated post

Requirements:
  pip install playwright pytz
  playwright install chromium
  Set LI_EMAIL and LI_PASSWORD env vars OR use --email and --password flags.
"""

import os
import sys
import re
import json
import time
import argparse
import random
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

# ─── Configuration ────────────────────────────────────────────────────────────

AEST = ZoneInfo("Australia/Sydney")

# LinkedIn posting schedule (Mon=0, Wed=2, Fri=4)
POST_DAYS = {0, 2, 4}  # Monday, Wednesday, Friday
POST_HOUR = 8
POST_MINUTE = 0

# Credentials (use env vars — never hardcode)
LI_EMAIL = os.getenv("LI_EMAIL", "kinan@agileadapt.com.au")
LI_PASSWORD = os.getenv("LI_PASSWORD", "")

# Content paths
CONTENT_DIR = Path("E:/genesis-system/AGILEADAPT/marketing/posts")
GENERATED_DIR = CONTENT_DIR / "generated"
POST_LOG = Path("E:/genesis-system/data/linkedin_post_log.json")

# Screenshots
SCREENSHOT_DIR = Path("E:/genesis-system/data/screenshots")

# ─── Post Log ─────────────────────────────────────────────────────────────────

def get_posted_ids() -> set:
    """Load IDs of already-posted LinkedIn content."""
    if POST_LOG.exists():
        data = json.loads(POST_LOG.read_text())
        return set(data.get("posted_ids", []))
    return set()


def log_post(post_id: str, post_text: str, success: bool, error: str = None):
    """Record posting result to local JSON log."""
    record = {
        "post_id": post_id,
        "posted_at": datetime.now(AEST).isoformat(),
        "success": success,
        "error": error,
        "preview": post_text[:100] if post_text else "",
    }

    existing = json.loads(POST_LOG.read_text()) if POST_LOG.exists() else {"posted_ids": [], "records": []}
    if success and post_id not in existing["posted_ids"]:
        existing["posted_ids"].append(post_id)
    existing["records"].append(record)

    POST_LOG.parent.mkdir(parents=True, exist_ok=True)
    POST_LOG.write_text(json.dumps(existing, indent=2))
    print(f"Logged: {post_id} — {'OK' if success else 'FAILED'}")

# ─── Content Parser ───────────────────────────────────────────────────────────

def parse_post_file(filepath: Path) -> dict:
    """Parse a markdown post file. Returns dict with content and metadata."""
    try:
        text = filepath.read_text(encoding="utf-8")
        result = {"filepath": str(filepath), "raw": text}

        for field in ["Platform", "Brand", "Topic", "Generated"]:
            match = re.search(rf"\*\*{field}:\*\*\s*(.+)", text)
            if match:
                result[field.lower()] = match.group(1).strip()

        parts = text.split("---", 1)
        result["content"] = parts[1].strip() if len(parts) > 1 else text.strip()
        return result
    except Exception as e:
        print(f"Error parsing {filepath}: {e}")
        return None


def get_next_li_post(posted_ids: set) -> dict:
    """Find next unposted LinkedIn content."""
    # Check pre-written posts first
    li_files = sorted(CONTENT_DIR.glob("li_agileadapt_*.md"))
    for f in li_files:
        if f.stem not in posted_ids:
            parsed = parse_post_file(f)
            if parsed and parsed.get("content"):
                return {"id": f.stem, "file": f, "data": parsed}

    # Then generated posts
    if GENERATED_DIR.exists():
        for f in sorted(GENERATED_DIR.glob("agileadapt_linkedin_*.md")):
            if f.stem not in posted_ids:
                parsed = parse_post_file(f)
                if parsed and parsed.get("content"):
                    return {"id": f.stem, "file": f, "data": parsed}

    return None

# ─── Scheduler ────────────────────────────────────────────────────────────────

def get_next_post_datetime() -> datetime:
    """Calculate next Mon/Wed/Fri 8am AEST posting slot."""
    now = datetime.now(AEST)
    candidate = now.replace(hour=POST_HOUR, minute=POST_MINUTE, second=0, microsecond=0)

    for days_ahead in range(7):
        check = candidate + timedelta(days=days_ahead)
        if check.weekday() in POST_DAYS and check > now + timedelta(minutes=5):
            return check

    # Fallback: next Monday
    days_until_monday = (7 - now.weekday()) % 7 or 7
    return candidate + timedelta(days=days_until_monday)


def wait_until(target: datetime):
    """Sleep until target datetime."""
    now = datetime.now(AEST)
    wait_sec = (target - now).total_seconds()
    if wait_sec > 0:
        print(f"Waiting {int(wait_sec / 60)} minutes until {target.strftime('%A %Y-%m-%d %H:%M AEST')}...")
        time.sleep(wait_sec)

# ─── Playwright LinkedIn Poster ───────────────────────────────────────────────

def post_to_linkedin(post_text: str, email: str, password: str) -> dict:
    """
    Post content to LinkedIn using Playwright.
    Uses full desktop browser flow (not mobile).
    Returns dict: {success: bool, error: str}
    """
    from playwright.sync_api import sync_playwright

    result = {"success": False, "error": None}
    SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
    ts = int(time.time())

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
            viewport={"width": 1280, "height": 800},
        )
        page = context.new_page()

        try:
            # ── Step 1: Login ──────────────────────────────────────────────
            print("Navigating to LinkedIn login...")
            page.goto("https://www.linkedin.com/login", timeout=20000)
            page.wait_for_load_state("networkidle", timeout=15000)

            if page.is_visible("input#username"):
                page.fill("input#username", email)
                page.fill("input#password", password)
                page.click("button[type='submit']")
                page.wait_for_load_state("networkidle", timeout=15000)
                time.sleep(2)

            # Check for verification / CAPTCHA
            if "checkpoint" in page.url or "challenge" in page.url or "captcha" in page.url:
                result["error"] = "LinkedIn checkpoint/CAPTCHA triggered — manual intervention needed"
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_checkpoint_{ts}.png"))
                return result

            if "feed" not in page.url and "linkedin.com/in/" not in page.url:
                result["error"] = f"Login may have failed — URL: {page.url}"
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_login_fail_{ts}.png"))
                return result

            print("Login successful.")

            # ── Step 2: Open Post Composer ─────────────────────────────────
            print("Opening post composer...")
            page.goto("https://www.linkedin.com/feed/", timeout=15000)
            page.wait_for_load_state("networkidle", timeout=10000)
            time.sleep(2)

            # Click "Start a post" button
            start_post_selectors = [
                "button.share-box-feed-entry__trigger",
                "[data-test-id='share-new-post-btn']",
                "//span[text()='Start a post']",
                "button:has-text('Start a post')",
                ".share-entry-point button",
            ]

            composer_opened = False
            for selector in start_post_selectors:
                try:
                    if page.is_visible(selector, timeout=3000):
                        page.click(selector)
                        composer_opened = True
                        time.sleep(2)
                        break
                except Exception:
                    continue

            if not composer_opened:
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_no_composer_{ts}.png"))
                result["error"] = "Could not open post composer"
                return result

            # ── Step 3: Type Post Content ──────────────────────────────────
            print("Typing post content...")

            text_area_selectors = [
                "div.ql-editor",
                "div[role='textbox']",
                "div.share-creation-state__content",
                "[data-test-id='share-form-update-textarea']",
            ]

            typed = False
            for selector in text_area_selectors:
                try:
                    if page.is_visible(selector, timeout=5000):
                        page.click(selector)
                        time.sleep(0.5)
                        # Type in chunks to simulate human input
                        for chunk in [post_text[i:i+80] for i in range(0, len(post_text), 80)]:
                            page.keyboard.type(chunk)
                            time.sleep(random.uniform(0.05, 0.2))
                        typed = True
                        break
                except Exception:
                    continue

            if not typed:
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_no_textarea_{ts}.png"))
                result["error"] = "Could not find or interact with text area"
                return result

            time.sleep(1)

            # ── Step 4: Click Post Button ──────────────────────────────────
            print("Submitting post...")

            post_button_selectors = [
                "button.share-actions__primary-action",
                "button[aria-label='Post']",
                "//button[text()='Post']",
                "button:has-text('Post')",
                "[data-test-id='share-form-post-btn']",
            ]

            posted = False
            for selector in post_button_selectors:
                try:
                    if page.is_visible(selector, timeout=3000):
                        page.click(selector)
                        posted = True
                        time.sleep(3)
                        break
                except Exception:
                    continue

            if posted:
                page.wait_for_load_state("networkidle", timeout=10000)
                time.sleep(2)
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_success_{ts}.png"))
                result["success"] = True
                print("Post submitted successfully.")
            else:
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_no_postbtn_{ts}.png"))
                result["error"] = "Post button not found"

        except Exception as e:
            result["error"] = str(e)
            try:
                page.screenshot(path=str(SCREENSHOT_DIR / f"li_error_{ts}.png"))
            except Exception:
                pass
        finally:
            browser.close()

    return result

# ─── Main ─────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="LinkedIn Publisher — AgileAdapt/ReceptionistAI")
    parser.add_argument("--post-now", action="store_true", help="Post immediately")
    parser.add_argument("--schedule", action="store_true", help="Wait for next Mon/Wed/Fri 8am AEST")
    parser.add_argument("--post-file", type=str, help="Path to specific post file")
    parser.add_argument("--email", type=str, help="LinkedIn email (overrides env)")
    parser.add_argument("--password", type=str, help="LinkedIn password (overrides env)")
    parser.add_argument("--dry-run", action="store_true", help="Print post without posting")
    args = parser.parse_args()

    email = args.email or LI_EMAIL
    password = args.password or LI_PASSWORD

    if not args.dry_run and not password:
        print("Error: LinkedIn password required. Set LI_PASSWORD env var or use --password")
        sys.exit(1)

    # Resolve post content
    if args.post_file:
        parsed = parse_post_file(Path(args.post_file))
        if not parsed:
            print(f"Error: Could not parse {args.post_file}")
            sys.exit(1)
        post_text = parsed["content"]
        post_id = Path(args.post_file).stem
    else:
        posted_ids = get_posted_ids()
        next_post = get_next_li_post(posted_ids)
        if not next_post:
            print("No unposted LinkedIn content found. Run social_post_generator.py --batch --platform linkedin --brand agileadapt")
            sys.exit(0)
        post_text = next_post["data"]["content"]
        post_id = next_post["id"]

    print(f"\nPost ID: {post_id}")
    print(f"Preview: {post_text[:150]}...\n")

    if args.dry_run:
        print("--- DRY RUN ---")
        print(post_text)
        print("--- END ---")
        return

    # Scheduling
    if args.schedule:
        next_time = get_next_post_datetime()
        wait_until(next_time)

    # Post
    print(f"Posting to LinkedIn ({email})...")
    result = post_to_linkedin(post_text, email, password)

    if result["success"]:
        print("LinkedIn post published successfully.")
        log_post(post_id, post_text, True)
    else:
        print(f"LinkedIn posting failed: {result.get('error', 'Unknown')}")
        log_post(post_id, post_text, False, result.get("error"))
        sys.exit(1)


if __name__ == "__main__":
    main()
