from __future__ import annotations

import argparse
import csv
import json
import re
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional

from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
from rich.panel import Panel
from rich.text import Text

from .models import CandidateProfile
from .orchestrator import JobSearchOrchestrator
from .sources import RemoteOKSource, RemotiveSource, WeWorkRemotelySource, LinkedInJobSource
from .automation import (
    EmailSender,
    LinkedInCompanyResolver,
    LinkedInEasyApplyApplier,
    LinkedInEmployeeCollector,
    PlaywrightSession,
)
from .automation.export_auth import export_storage_state
from .terminal_ui import (
    header,
    console,
    job_summary_table,
    render_cover_letter,
    render_interview_questions,
    render_portfolio_suggestions,
    render_tailored_resume,
    section,
    show_warnings,
    print_banner,
)


def load_candidate_from_json(path: Path) -> CandidateProfile:
    data: Dict[str, Any] = json.loads(path.read_text())
    return CandidateProfile(
        name=data["name"],
        email=data["email"],
        headline=data["headline"],
        years_experience=int(data["years_experience"]),
        skills=data["skills"],
        industries=data.get("industries", []),
        tools=data.get("tools", []),
        achievements=data.get("achievements", []),
        resume_text=data.get("resume_text"),
        portfolio_links=data.get("portfolio_links", []),
    )


def _parse_comma_separated(value: Optional[str]) -> List[str]:
    if not value:
        return []
    return [item.strip() for item in value.split(",") if item.strip()]


def _infer_skills_from_text(resume_text: str) -> List[str]:
    # Heuristic extraction: look for "Skills:" / "Technologies:" headings and parse until the next section.
    candidates: List[str] = []
    lower = resume_text.lower()
    headings = ["skills", "technologies", "tech stack", "tools"]

    section_stoppers = [
        "experience",
        "work experience",
        "employment",
        "education",
        "projects",
        "certifications",
        "summary",
        "profile",
    ]

    for heading in headings:
        # Pattern 1: "Skills: Python, SQL, AWS"
        m_inline = re.search(rf"{re.escape(heading)}\s*:\s*(.+)", resume_text, flags=re.IGNORECASE)
        if m_inline:
            line = m_inline.group(1).strip()
            parts = re.split(r"[,|/]|•|\n", line)
            candidates.extend([p.strip() for p in parts if p.strip()])
            continue

        # Pattern 2: heading on its own line; take subsequent lines until a stopper/blank.
        m_heading = re.search(rf"(?m)^{re.escape(heading)}\s*$", resume_text, flags=re.IGNORECASE)
        if not m_heading:
            continue

        start = m_heading.end()
        tail = resume_text[start:]
        tail_lines = tail.splitlines()
        for line in tail_lines:
            if not line.strip():
                break
            if any(stopper in line.lower() for stopper in section_stoppers):
                break
            # split bullets/commas
            parts = re.split(r"[,|/]|•", line)
            candidates.extend([p.strip() for p in parts if p.strip()])

    # Normalize and de-duplicate.
    out: List[str] = []
    seen = set()
    for c in candidates:
        c2 = c.strip()
        if not c2:
            continue
        key = c2.lower()
        if key in seen:
            continue
        seen.add(key)
        out.append(c2)
    return out


def _slugify(value: str) -> str:
    value = value.strip().lower()
    value = re.sub(r"[^a-z0-9]+", "-", value)
    value = re.sub(r"-{2,}", "-", value)
    return value.strip("-") or "job"


def load_candidate_from_text(
    resume_path: Path,
    cover_letter_path: Optional[Path],
    name: str,
    email: str,
    headline: str,
    years_experience: int,
    skills_csv: Optional[str],
    portfolio_links: Optional[List[str]],
) -> CandidateProfile:
    resume_text = resume_path.read_text()
    cover_letter_text = cover_letter_path.read_text() if cover_letter_path else None

    skills = _parse_comma_separated(skills_csv)
    if not skills:
        skills = _infer_skills_from_text(resume_text)

    achievements: List[str] = []
    for line in resume_text.splitlines():
        stripped = line.strip()
        if stripped.startswith(("-", "•")):
            achievements.append(stripped.lstrip("-• ").strip())

    combined_resume_text = resume_text
    if cover_letter_text:
        combined_resume_text = resume_text + "\n\n--- COVER LETTER ---\n\n" + cover_letter_text

    return CandidateProfile(
        name=name,
        email=email,
        headline=headline,
        years_experience=years_experience,
        skills=skills,
        achievements=achievements,
        resume_text=combined_resume_text,
        portfolio_links=portfolio_links or [],
    )


def cmd_full(args: argparse.Namespace) -> None:
    jd_path = Path(args.job_description)
    raw_jd = jd_path.read_text()

    if args.candidate_json:
        candidate = load_candidate_from_json(Path(args.candidate_json))
    else:
        resume_path = Path(args.resume_file)
        cover_letter_path = Path(args.cover_letter_file) if args.cover_letter_file else None
        missing = []
        for field_name in ["name", "email", "headline", "years_experience"]:
            if getattr(args, field_name) is None:
                missing.append(field_name)
        if missing:
            raise SystemExit(f"Missing required fields for text resume mode: {', '.join(missing)}")
        if not args.skills:
            # Skills will be inferred from resume text automatically.
            pass
        candidate = load_candidate_from_text(
            resume_path=resume_path,
            cover_letter_path=cover_letter_path,
            name=args.name,
            email=args.email,
            headline=args.headline,
            years_experience=args.years_experience,
            skills_csv=args.skills,
            portfolio_links=args.portfolio_link or [],
        )

    orchestrator = JobSearchOrchestrator()
    with console.status("[bold green]Agentic Workflow Running (Generating Outputs...)[/]"):
        result = orchestrator.full_pipeline(raw_jd, candidate)

    job = result["job"]
    tailored_resume = result["tailored_resume"]
    tailored_cover = result["tailored_cover_letter"]
    portfolio_suggestions = result["portfolio_suggestions"]
    interview_session = result["interview_session"]

    header("Job Description Parsed")
    job_summary_table(job)

    render_tailored_resume(tailored_resume)
    render_cover_letter(tailored_cover)
    render_portfolio_suggestions(portfolio_suggestions)
    render_interview_questions(interview_session.turns)

    show_warnings(tailored_resume.warnings)
    show_warnings(tailored_cover.warnings)


def _write_text_file(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content)


def _format_tailored_resume(tailored_resume: object) -> str:
    lines: List[str] = []
    summary = getattr(tailored_resume, "summary", "") or ""
    lines.append("TAILORED RESUME SUMMARY")
    lines.append(summary)
    lines.append("")
    highlighted = getattr(tailored_resume, "highlighted_skills", []) or []
    if highlighted:
        lines.append("HIGHLIGHTED SKILLS: " + ", ".join(highlighted))
        lines.append("")
    bullets = getattr(tailored_resume, "bullet_points", []) or []
    if bullets:
        lines.append("SUGGESTED BULLETS:")
        for b in bullets:
            lines.append(f"- {b}")
        lines.append("")
    warnings = getattr(tailored_resume, "warnings", []) or []
    if warnings:
        lines.append("WARNINGS:")
        for w in warnings:
            lines.append(f"! {w}")
        lines.append("")
    return "\n".join(lines).strip() + "\n"


def _format_cover_letter(tailored_cover: object) -> str:
    intro = getattr(tailored_cover, "intro", "") or ""
    body = getattr(tailored_cover, "body", "") or ""
    closing = getattr(tailored_cover, "closing", "") or ""
    warnings = getattr(tailored_cover, "warnings", []) or []

    lines = ["TAILORED COVER LETTER", "", intro, "", body, "", closing]
    if warnings:
        lines.append("")
        lines.append("WARNINGS:")
        for w in warnings:
            lines.append(f"! {w}")
    return "\n".join(lines).strip() + "\n"


def _format_portfolio_suggestions(suggestions: Iterable[object]) -> str:
    lines: List[str] = ["PORTFOLIO SUGGESTIONS", ""]
    for s in suggestions:
        priority = str(getattr(s, "priority", "") or "").upper()
        title = str(getattr(s, "title", "") or "")
        description = str(getattr(s, "description", "") or "")
        lines.append(f"[{priority}] {title}")
        lines.append(description)
        lines.append("")
    return "\n".join(lines).strip() + "\n"


def _format_interview_questions(turns: Iterable[object]) -> str:
    lines: List[str] = ["MOCK INTERVIEW QUESTIONS", ""]
    for idx, turn in enumerate(turns, start=1):
        q = getattr(turn, "question", turn)
        category = getattr(q, "category", "") or ""
        question = getattr(q, "question", "") or ""
        focus_areas = getattr(q, "focus_areas", []) or []
        lines.append(f"Q{idx} ({category}): {question}")
        if focus_areas:
            lines.append("Focus: " + ", ".join(focus_areas))
        lines.append("")
    return "\n".join(lines).strip() + "\n"


def _get_text_mode_candidate(args: argparse.Namespace) -> CandidateProfile:
    missing = []
    if not args.resume_file:
        missing.append("resume_file")
    for field_name in ["name", "email", "headline", "years_experience"]:
        if getattr(args, field_name) is None:
            missing.append(field_name)
    if missing:
        raise SystemExit(f"Missing required fields for text resume mode: {', '.join(missing)}")
    resume_path = Path(args.resume_file)
    cover_letter_path = Path(args.cover_letter_file) if args.cover_letter_file else None
    return load_candidate_from_text(
        resume_path=resume_path,
        cover_letter_path=cover_letter_path,
        name=args.name,
        email=args.email,
        headline=args.headline,
        years_experience=args.years_experience,
        skills_csv=args.skills,
        portfolio_links=args.portfolio_link or [],
    )


def cmd_run(args: argparse.Namespace) -> None:
    # Source -> scan roles -> tailor for each job -> package outputs.
    if args.candidate_json:
        candidate = load_candidate_from_json(Path(args.candidate_json))
    else:
        candidate = _get_text_mode_candidate(args)

    source_name = (args.source or "remoteok").lower()
    if source_name == "remoteok":
        source = RemoteOKSource()
    elif source_name == "remotive":
        source = RemotiveSource()
    elif source_name in {"weworkremotely", "wework-remote"}:
        source = WeWorkRemotelySource()
    elif source_name == "linkedin":
        if not args.linkedin_storage_state:
            raise SystemExit("--linkedin-storage-state is required when --source linkedin")
        source = LinkedInJobSource(storage_state=Path(args.linkedin_storage_state))
    else:
        raise SystemExit(
            f"Unsupported source '{args.source}'. Supported: remoteok, remotive, weworkremotely, linkedin."
        )

    jobs: List[object] = []
    header("Scanning Jobs")
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TimeElapsedColumn(),
    ) as progress:
        scan_task = progress.add_task("Fetch + filter listings", total=None)
        jobs = source.search(query=args.query, max_jobs=args.max_jobs)
        progress.update(scan_task, completed=1)

    if not jobs:
        header("No jobs matched your query")
        return

    orchestrator = JobSearchOrchestrator()
    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    match_summary: List[str] = []

    for idx, job_hint in enumerate(jobs, start=1):
        job_title = getattr(job_hint, "title", "") or "Untitled"
        job_company = getattr(job_hint, "company", "") or "Unknown"
        job_slug = _slugify(f"{job_company}-{job_title}")[:80]

        header(f"Job {idx}/{len(jobs)}: {job_title}")
        job_summary_table(job_hint)

        with console.status(f"[bold green]Generating Tailored Content for {job_title}...[/]"):
            result = orchestrator.full_pipeline_for_job(job_hint, candidate)
        job = result["job"]
        tailored_resume = result["tailored_resume"]
        tailored_cover = result["tailored_cover_letter"]
        portfolio_suggestions = result["portfolio_suggestions"]
        interview_session = result["interview_session"]

        render_tailored_resume(tailored_resume)
        render_cover_letter(tailored_cover)
        render_portfolio_suggestions(portfolio_suggestions)
        render_interview_questions(interview_session.turns)

        # Optional interactive interview simulation.
        if args.interactive_interview:
            from copy import deepcopy

            session = deepcopy(interview_session)
            answers: List[str] = []
            for turn_idx, turn in enumerate(session.turns, start=1):
                q = turn.question.question
                section(f"Your answer for Q{turn_idx}")
                answer = input(f"{q}\n\nAnswer: ").strip()
                answers.append(answer)
            session = orchestrator.review_mock_interview(session, answers)
            # Show feedback for the turns.
            section("Interview Feedback")
            for turn_idx, turn in enumerate(session.turns, start=1):
                console_feedback = f"Q{turn_idx} Score: {turn.score}/5" if turn.score is not None else f"Q{turn_idx}"
                show_warnings([console_feedback])
                if turn.feedback:
                    print(turn.feedback)

            interview_session = session

        # Package outputs.
        section("Writing Files")
        job_out_dir = output_dir / job_slug
        _write_text_file(job_out_dir / "job_description.txt", getattr(job, "raw_text", "") or "")
        _write_text_file(job_out_dir / "job_url.txt", getattr(job, "url", "") or "")
        _write_text_file(job_out_dir / "job_title.txt", getattr(job, "title", "") or "")
        _write_text_file(job_out_dir / "job_company.txt", getattr(job, "company", "") or "")
        _write_text_file(job_out_dir / "job_location.txt", getattr(job, "location", "") or "")
        _write_text_file(job_out_dir / "tailored_resume.txt", _format_tailored_resume(tailored_resume))
        _write_text_file(job_out_dir / "tailored_cover_letter.txt", _format_cover_letter(tailored_cover))
        _write_text_file(
            job_out_dir / "portfolio_suggestions.txt", _format_portfolio_suggestions(portfolio_suggestions)
        )
        _write_text_file(
            job_out_dir / "mock_interview.txt",
            _format_interview_questions(getattr(interview_session, "turns", [])),
        )

        match_summary.append(
            f"{idx}. {getattr(job, 'title', '')} @ {getattr(job, 'company', '')}: match={len(getattr(tailored_resume, 'highlighted_skills', []) or [])}"
        )

    header("Done")
    # Keep this summary human-readable; individual packages are written to disk.
    for line in match_summary:
        print(line)


def _confirm(prompt: str, *, default_no: bool = True) -> bool:
    suffix = " [y/N]: " if default_no else " [Y/n]: "
    resp = input(prompt + suffix).strip().lower()
    if not resp:
        return not default_no
    return resp in {"y", "yes"}


def cmd_apply(args: argparse.Namespace) -> None:
    input_dir = Path(args.input_dir)
    storage_state = Path(args.storage_state)

    if not input_dir.exists():
        raise SystemExit(f"Input dir not found: {input_dir}")
    if not storage_state.exists():
        raise SystemExit(f"storage_state not found: {storage_state}")
    if not args.resume_file:
        raise SystemExit("--resume-file is required for apply automation")

    resume_file = Path(args.resume_file)
    cover_letter_file = Path(args.cover_letter_file) if args.cover_letter_file else None

    def confirm_fn(msg: str) -> bool:
        console.print(Panel.fit(Text("Confirm Submission", style="bold green")))
        print(msg)
        return _confirm("Submit now?")

    jobs = [p for p in input_dir.iterdir() if p.is_dir()]
    if not jobs:
        raise SystemExit(f"No job packages found under: {input_dir}")

    header("Authenticated Apply (Best-Effort)")

    def job_fn(page, *, job_package: Path) -> None:
        job_url = (job_package / "job_url.txt").read_text().strip()
        job_title = (job_package / "job_title.txt").read_text().strip() if (job_package / "job_title.txt").exists() else ""
        shots_dir = job_package / "screenshots"
        applier_local = LinkedInEasyApplyApplier(
            resume_file=resume_file,
            cover_letter_file=cover_letter_file,
            screenshots_dir=shots_dir,
        )

        if not job_url:
            console.print(f"[yellow]Skipping (missing job_url.txt): {job_package.name}[/yellow]")
            return

        if "linkedin.com/jobs" in job_url:
            result = applier_local.apply(page=page, job_url=job_url, confirm_before_final_submit=confirm_fn)
            status = "SUBMITTED" if result.submitted else "NOT SUBMITTED"
            console.print(f"[{'green' if result.submitted else 'red'}]{status}[/]")
            if result.notes:
                console.print(f"[dim]{result.notes}[/dim]")
        else:
            # Generic fallback: open URL and require user to apply manually.
            snap_msg = f"Opened job URL for manual completion:\n{job_url}"
            console.print(f"[yellow]{snap_msg}[/yellow]")
            page.goto(job_url, wait_until="domcontentloaded")
            _ = input("Complete the application manually, then press Enter here to continue.")

    # Playwright session.
    session = PlaywrightSession(storage_state=storage_state)
    for job_package in jobs:
        # We run one job per session invocation to keep context isolated and make troubleshooting simpler.
        session.run(
            job_fn=lambda page, jp=job_package: job_fn(page, job_package=jp),
        )

    header("Apply Finished")


def cmd_apply_and_reach_out(args: argparse.Namespace) -> None:
    input_dir = Path(args.input_dir)
    storage_state = Path(args.storage_state)

    if not input_dir.exists():
        raise SystemExit(f"Input dir not found: {input_dir}")
    if not storage_state.exists():
        raise SystemExit(f"storage_state not found: {storage_state}")
    if not args.resume_file:
        raise SystemExit("--resume-file is required for apply-and-reach-out automation")

    resume_file = Path(args.resume_file)
    cover_letter_file = Path(args.cover_letter_file) if args.cover_letter_file else None

    max_contacts = args.max_contacts
    scrape_emails = bool(args.scrape_emails)

    company_resolver = LinkedInCompanyResolver()
    collector = LinkedInEmployeeCollector()
    email_sender = EmailSender()

    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    tailored_summary_marker = "TAILORED RESUME SUMMARY"
    default_template = (
        "Hi {recipient_name},\n\n"
        "I’m applying for the {job_title} role at {job_company}.\n"
        "Based on my background ({candidate_headline}), I believe I can contribute quickly—especially across {skills}.\n\n"
        "If you’re open to it, I’d appreciate any quick tips on what helps people succeed in this team.\n\n"
        "Thanks,\n{candidate_name}"
    )

    def confirm_job(msg: str) -> bool:
        console.print(Panel.fit(Text("Apply + Reach Out", style="bold green")))
        print(msg)
        return _confirm("Proceed with this job?", default_no=True)

    job_packages = [p for p in input_dir.iterdir() if p.is_dir()]
    if not job_packages:
        raise SystemExit(f"No job packages found under: {input_dir}")

    header("Apply + Reach Out (Authenticated, Best-Effort)")
    session = PlaywrightSession(storage_state=storage_state)

    for job_package in job_packages:
        job_title = _read_text(job_package / "job_title.txt")
        job_company = _read_text(job_package / "job_company.txt")
        job_location = _read_text(job_package / "job_location.txt")
        job_url = _read_text(job_package / "job_url.txt")
        job_description_text = _read_text(job_package / "job_description.txt")

        if not job_company:
            # Best-effort: infer company from the saved job description text.
            try:
                from .agents.jd_parser import JobDescriptionParserAgent

                inferred = JobDescriptionParserAgent().parse(job_description_text).company or ""
                job_company = inferred
            except Exception:
                job_company = ""

        header(f"Job: {job_title or job_package.name} @ {job_company or 'Unknown Company'}")

        if not confirm_job(
            f"This will (1) attempt apply to the job (LinkedIn Easy Apply if possible) and (2) resolve the company on LinkedIn "
            f"to collect up to {max_contacts} contacts and draft emails.\n\nJob URL: {job_url or '(missing)'}"
        ):
            continue

        job_out_dir = job_package
        screenshots_dir = output_dir / f"{job_package.name}" / "screenshots"
        applier_local = LinkedInEasyApplyApplier(
            resume_file=resume_file,
            cover_letter_file=cover_letter_file,
            screenshots_dir=screenshots_dir,
        )

        # Phase 1: apply (if LinkedIn job URL).
        if job_url and "linkedin.com/jobs" in job_url:

            def confirm_final_submit(message: str) -> bool:
                console.print(Panel.fit(Text("Final Submission Confirmation", style="bold green")))
                print(message)
                return _confirm("Submit now?", default_no=True)

            # Apply using the same Playwright session page.
            def apply_fn(page) -> None:
                applier_local.apply(
                    page=page,
                    job_url=job_url,
                    confirm_before_final_submit=confirm_final_submit,
                )

            session.run(job_fn=apply_fn)
        elif job_url:
            console.print(f"[yellow]Non-LinkedIn job URL. Opening for manual apply:[/yellow] {job_url}")
            # Open once; user completes manually.
            def open_job(page) -> None:
                page.goto(job_url, wait_until="domcontentloaded")
            session.run(job_fn=open_job)
            _ = input("Complete application manually, then press Enter here to continue.")

        # Phase 2: resolve company + collect contacts (and scrape emails if enabled).
        contacts_csv_path = job_out_dir / "contacts.csv"

        resolved_company_url: Optional[str] = None

        def company_and_contacts_fn(page) -> None:
            nonlocal resolved_company_url
            if not job_company:
                resolved_company_url = None
                return
            resolve_result = company_resolver.resolve(page=page, company_name=job_company)
            resolved_company_url = resolve_result.company_url
            if not resolved_company_url:
                return
            try:
                screenshots_dir.mkdir(parents=True, exist_ok=True)
                page.screenshot(
                    path=str(screenshots_dir / "01_company_search.png"),
                    full_page=True,
                )
            except Exception:
                pass
            contacts = collector.collect(
                page=page,
                company_url=resolved_company_url,
                max_contacts=max_contacts,
                confirm_before_start=lambda _m: True,
                scrape_emails=scrape_emails,
                screenshots_dir=screenshots_dir,
            )
            collector.write_contacts_csv(contacts, contacts_csv_path)

        # Use a single new browser context per phase for isolation.
        session.run(job_fn=company_and_contacts_fn)

        if not contacts_csv_path.exists():
            console.print(f"[yellow]No contacts collected for {job_company} (see screenshots).[/yellow]")
            continue

        # Phase 3: draft + optionally send emails.
        tailored_resume_txt = _read_text(job_package / "tailored_resume.txt")
        resume_summary_line = _extract_first_line_after_marker(
            tailored_resume_txt, tailored_summary_marker
        )

        # Skills for message: prefer extracted from CLI args, otherwise fall back to inferred from resume summary.
        skills_for_email = args.skills or resume_summary_line or "my relevant experience"

        candidate_name = args.name or "there"
        candidate_headline = args.headline or ""
        subject = args.subject or f"Quick question about {job_title or job_company or 'your team'}"
        template = args.template or default_template

        drafts_dir = output_dir / f"{job_package.name}" / "drafts"
        drafts_dir.mkdir(parents=True, exist_ok=True)

        with contacts_csv_path.open("r", newline="") as f:
            reader = csv.DictReader(f)
            for idx, row in enumerate(reader, start=1):
                recipient_name = row.get("name") or ""
                recipient_email = row.get("email") or ""
                profile_url = row.get("profile_url") or ""
                if not recipient_name:
                    continue

                body = template.format(
                    recipient_name=recipient_name,
                    job_title=job_title or "this role",
                    job_company=job_company or "your company",
                    job_location=job_location,
                    candidate_name=candidate_name,
                    candidate_headline=candidate_headline,
                    skills=skills_for_email,
                    profile_url=profile_url,
                )

                (drafts_dir / f"{idx:03d}_{recipient_name.replace(' ', '_')}.txt").write_text(
                    f"TO: {recipient_name}\nEMAIL: {recipient_email}\nPROFILE: {profile_url}\nSUBJECT: {subject}\n\n{body}\n"
                )

                if args.send_emails and recipient_email:
                    console.print(f"[bold]Ready to open mail client for:[/bold] {recipient_name} <{recipient_email}>")
                    if _confirm("Open mailto for this email (send manually)?"):
                        from .automation.email_sender import EmailDraft

                        email_sender.open_mailto(
                            EmailDraft(to=recipient_email, subject=subject, body=body)
                        )

    header("Apply + Reach Out Finished")


def cmd_collect_contacts(args: argparse.Namespace) -> None:
    """
    Collect employee profile links from LinkedIn company pages into a CSV.
    """
    from .automation.linkedin_contacts import LinkedInEmployeeCollector

    storage_state = Path(args.storage_state)
    if not storage_state.exists():
        raise SystemExit(f"storage_state not found: {storage_state}")

    companies_file = Path(args.companies_file)
    if not companies_file.exists():
        raise SystemExit(f"companies_file not found: {companies_file}")

    out_csv = Path(args.output_csv)
    max_contacts = args.max_contacts

    collector = LinkedInEmployeeCollector()

    companies: List[str] = []
    for line in companies_file.read_text().splitlines():
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        companies.append(s)

    if not companies:
        raise SystemExit("No company URLs found.")

    header("Collect Contacts (LinkedIn - Best Effort)")

    def confirm_fn(msg: str) -> bool:
        console.print(Panel.fit(Text("Confirm Contact Scan", style="bold magenta")))
        print(msg)
        return _confirm("Continue?")

    all_rows = []
    session = PlaywrightSession(storage_state=storage_state)
    for company_url in companies:
        contacts: List[object] = []

        def job_fn(page) -> None:
            nonlocal contacts
            contacts = collector.collect(
                page=page,
                company_url=company_url,
                max_contacts=max_contacts,
                confirm_before_start=confirm_fn,
            )

        session.run(job_fn=job_fn)
        all_rows.extend(contacts)

    collector.write_contacts_csv(all_rows, out_csv)
    header(f"Wrote contacts: {out_csv}")


def _read_text(path: Path) -> str:
    return path.read_text().strip() if path.exists() else ""


def _extract_first_line_after_marker(content: str, marker: str) -> str:
    lines = content.splitlines()
    for i, line in enumerate(lines):
        if marker in line:
            if i + 1 < len(lines):
                return lines[i + 1].strip()
    return ""


def cmd_message_contacts(args: argparse.Namespace) -> None:
    """
    Draft messages and optionally open mailto for sending (with per-recipient confirmation).
    """
    contacts_csv = Path(args.contacts_csv)
    if not contacts_csv.exists():
        raise SystemExit(f"contacts_csv not found: {contacts_csv}")

    job_package_dir = Path(args.job_package_dir)
    if not job_package_dir.exists():
        raise SystemExit(f"job_package_dir not found: {job_package_dir}")

    tailored_resume_txt = _read_text(job_package_dir / "tailored_resume.txt")
    job_title = _read_text(job_package_dir / "job_title.txt")
    job_company = _read_text(job_package_dir / "job_company.txt")
    job_location = _read_text(job_package_dir / "job_location.txt")

    candidate_name = args.name or "there"
    candidate_headline = args.headline or ""

    resume_summary = _extract_first_line_after_marker(
        tailored_resume_txt, "TAILORED RESUME SUMMARY"
    )
    skills_for_email = args.skills or ""

    template = args.template or (
        "Hi {recipient_name},\n\n"
        "I’m applying for the {job_title} role at {job_company}.\n"
        "From my background ({candidate_headline}), I believe I can contribute quickly—especially across {skills}.\n\n"
        "If you’re open to it, I’d appreciate any quick tips on what helped others succeed in this team.\n\n"
        "Thanks,\n{candidate_name}"
    )

    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    draft_dir = out_dir / "drafts"
    draft_dir.mkdir(parents=True, exist_ok=True)

    email_sender = EmailSender()

    with contacts_csv.open("r", newline="") as f:
        reader = csv.DictReader(f)
        required_cols = {"name", "profile_url"}
        missing = required_cols - set(reader.fieldnames or [])
        if missing:
            raise SystemExit(f"contacts_csv missing columns: {', '.join(sorted(missing))}")

        sent_count = 0
        drafted_count = 0
        for idx, row in enumerate(reader, start=1):
            recipient_name = row.get("name") or ""
            profile_url = row.get("profile_url") or ""
            email = row.get(args.email_col) if args.email_col else (row.get("email") or "")

            if not recipient_name:
                continue

            body = template.format(
                recipient_name=recipient_name,
                job_title=job_title or "this role",
                job_company=job_company or "your company",
                job_location=job_location,
                candidate_name=candidate_name,
                candidate_headline=candidate_headline,
                skills=skills_for_email or resume_summary or "my relevant experience",
                profile_url=profile_url,
            )

            subject = args.subject or f"Quick question about {job_title or 'your team'}"

            draft = {
                "recipient_name": recipient_name,
                "profile_url": profile_url,
                "email": email,
                "subject": subject,
                "body": body,
            }
            (draft_dir / f"{idx:03d}_{recipient_name.replace(' ', '_')}.txt").write_text(
                f"TO: {recipient_name}\nPROFILE: {profile_url}\nEMAIL: {email}\nSUBJECT: {subject}\n\n{body}\n"
            )
            drafted_count += 1

            if args.send and email:
                console.print(f"[bold]Ready to send to:[/bold] {recipient_name} <{email}>")
                if _confirm("Open mail client and you will send manually?"):
                    from .automation.email_sender import EmailDraft

                    email_sender.open_mailto(EmailDraft(to=email, subject=subject, body=body))
                    sent_count += 1

    header(f"Message drafts ready. Drafted={drafted_count}, Sent opened={sent_count}")


def cmd_export_auth(args: argparse.Namespace) -> None:
    """
    Export Playwright storage_state by letting user log in manually.
    """
    output_path = Path(args.output_path)
    login_url = args.login_url
    headless = bool(args.headless)

    header("Export Auth (storage_state)")
    export_storage_state(login_url=login_url, output_path=output_path, headless=headless)
    header(f"Exported storage_state to: {output_path}")


def cmd_autopilot(args: argparse.Namespace) -> None:
    """Start the autonomous job scanning, scoring, tailoring, and applying bot."""
    from .bot import JobBot
    from .models import BotConfig

    if args.candidate_json:
        candidate = load_candidate_from_json(Path(args.candidate_json))
    else:
        candidate = _get_text_mode_candidate(args)

    config = BotConfig(
        query=args.query,
        sources=[s.strip() for s in args.sources.split(",")],
        interval_minutes=args.interval,
        min_score=args.min_score,
        max_jobs_per_scan=args.max_jobs,
        output_dir=args.output_dir,
        max_packages=args.max_packages,
        max_log_lines=args.max_log_lines,
    )

    resume_file = Path(args.resume_file) if args.resume_file else None
    cover_letter_file = Path(args.cover_letter_file) if args.cover_letter_file else None
    storage_state = Path(args.storage_state) if args.storage_state else None

    bot = JobBot(
        candidate=candidate,
        config=config,
        resume_file=resume_file,
        cover_letter_file=cover_letter_file,
        storage_state=storage_state,
    )
    bot.run()

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="job-search",
        description="Agentic job search assistant (CLI).",
    )
    
    parser.add_argument(
        "--version",
        action="version",
        version="%(prog)s 0.1.0",
        help="Print the job-search Agentic CLI version and exit."
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    full = subparsers.add_parser(
        "full",
        help="Run the full pipeline: parse JD, tailor resume/cover letter, portfolio suggestions, mock interview questions.",
    )
    full.add_argument(
        "--job-description",
        "-j",
        required=True,
        help="Path to a text file containing the job description.",
    )
    # Option 1: JSON candidate profile (existing)
    full.add_argument(
        "--candidate-json",
        "-c",
        help="Path to a JSON file describing the candidate profile.",
    )
    # Option 2: resume / cover letter text files + metadata flags
    full.add_argument(
        "--resume-file",
        "-r",
        help="Path to a plain-text resume file.",
    )
    full.add_argument(
        "--cover-letter-file",
        help="Optional path to a plain-text cover letter file.",
    )
    full.add_argument(
        "--name",
        help="Candidate full name (required when using text resume mode).",
    )
    full.add_argument(
        "--email",
        help="Candidate email (required when using text resume mode).",
    )
    full.add_argument(
        "--headline",
        help="Short headline, e.g. 'Backend Engineer' (required when using text resume mode).",
    )
    full.add_argument(
        "--years-experience",
        type=int,
        dest="years_experience",
        help="Years of experience as an integer (required when using text resume mode).",
    )
    full.add_argument(
        "--skills",
        help="Comma-separated skills list, e.g. 'Python, PostgreSQL, AWS'.",
    )
    full.add_argument(
        "--portfolio-link",
        action="append",
        help="Portfolio link (can be passed multiple times).",
    )
    full.set_defaults(func=cmd_full)

    run = subparsers.add_parser(
        "run",
        help="Scan jobs from a source, tailor resume/cover letter per job, and save application packages.",
    )
    run.add_argument(
        "--source",
        default="remoteok",
        help="Job source to scan (default: remoteok). Supported: remoteok, remotive, weworkremotely, linkedin.",
    )
    run.add_argument(
        "--query",
        required=True,
        help="Query terms (e.g. 'python backend postgres').",
    )
    run.add_argument(
        "--max-jobs",
        type=int,
        default=5,
        dest="max_jobs",
        help="Maximum number of jobs to process.",
    )
    run.add_argument(
        "--output-dir",
        default="applications",
        help="Directory to write tailored outputs.",
    )
    run.add_argument(
        "--interactive-interview",
        action="store_true",
        help="Ask you for answers and generate feedback.",
        dest="interactive_interview",
    )
    # Candidate inputs
    run.add_argument(
        "--candidate-json",
        "-c",
        help="Optional candidate JSON profile (otherwise text resume mode).",
        dest="candidate_json",
    )
    run.add_argument(
        "--resume-file",
        "-r",
        help="Path to a plain-text resume file (required for text resume mode).",
        dest="resume_file",
    )
    run.add_argument(
        "--cover-letter-file",
        help="Optional path to a plain-text cover letter file.",
        dest="cover_letter_file",
    )
    run.add_argument("--name", help="Candidate full name (text resume mode).", dest="name")
    run.add_argument("--email", help="Candidate email (text resume mode).", dest="email")
    run.add_argument("--headline", help="Short headline (text resume mode).", dest="headline")
    run.add_argument(
        "--years-experience",
        type=int,
        dest="years_experience",
        help="Years of experience (text resume mode).",
    )
    run.add_argument("--skills", help="Comma-separated skills list.", dest="skills")
    run.add_argument(
        "--portfolio-link",
        action="append",
        help="Portfolio link (can be passed multiple times).",
        dest="portfolio_link",
    )
    run.add_argument(
        "--linkedin-storage-state",
        help="Required when --source linkedin. Path to Playwright storage_state JSON.",
        dest="linkedin_storage_state",
    )
    run.set_defaults(func=cmd_run)

    apply = subparsers.add_parser(
        "apply",
        help="Authenticated best-effort job application automation (LinkedIn Easy Apply) from stored job packages.",
    )
    apply.add_argument(
        "--input-dir",
        default="applications",
        help="Directory containing job packages (default: applications).",
        dest="input_dir",
    )
    apply.add_argument(
        "--storage-state",
        required=True,
        help="Playwright storage_state JSON path (created after you log in manually).",
        dest="storage_state",
    )
    apply.add_argument(
        "--resume-file",
        required=True,
        help="Path to a PDF/DOC resume file for upload.",
        dest="resume_file",
    )
    apply.add_argument(
        "--cover-letter-file",
        help="Optional cover letter file for upload.",
        dest="cover_letter_file",
    )
    apply.set_defaults(func=cmd_apply)

    collect = subparsers.add_parser(
        "collect-contacts",
        help="Authenticated best-effort LinkedIn employee profile collection from company pages.",
    )
    collect.add_argument(
        "--storage-state",
        required=True,
        help="Playwright storage_state JSON path (created after you log in manually).",
        dest="storage_state",
    )
    collect.add_argument(
        "--companies-file",
        required=True,
        help="Text file with one LinkedIn company URL per line.",
        dest="companies_file",
    )
    collect.add_argument(
        "--max-contacts",
        type=int,
        default=30,
        help="Max profiles to collect per company.",
        dest="max_contacts",
    )
    collect.add_argument(
        "--output-csv",
        required=True,
        help="Where to write contacts CSV.",
        dest="output_csv",
    )
    collect.set_defaults(func=cmd_collect_contacts)

    msg = subparsers.add_parser(
        "message-contacts",
        help="Draft quick emails to contacts and optionally open mailto for sending (with per-recipient confirmation).",
    )
    msg.add_argument("--contacts-csv", required=True, help="contacts.csv path.", dest="contacts_csv")
    msg.add_argument(
        "--job-package-dir",
        required=True,
        help="applications/<job-slug>/ directory holding tailored_resume.txt and job_title/job_company.",
        dest="job_package_dir",
    )
    msg.add_argument(
        "--output-dir",
        default="message_output",
        help="Directory to write message drafts.",
        dest="output_dir",
    )
    msg.add_argument("--name", help="Candidate name.", dest="name")
    msg.add_argument("--headline", help="Candidate headline (optional).", dest="headline")
    msg.add_argument(
        "--skills",
        help="Override skills to include in the message.",
        dest="skills",
    )
    msg.add_argument(
        "--template",
        help="Optional message template (supports placeholders).",
        dest="template",
    )
    msg.add_argument(
        "--subject",
        help="Optional email subject.",
        dest="subject",
    )
    msg.add_argument(
        "--email-col",
        help="CSV column name that contains recipient email addresses (optional).",
        dest="email_col",
    )
    msg.add_argument(
        "--send",
        action="store_true",
        help="If set, opens your mail client for recipients that have an email address (after confirmation).",
        dest="send",
    )
    msg.set_defaults(func=cmd_message_contacts)

    reach = subparsers.add_parser(
        "apply-and-reach-out",
        help="Attempt apply + automatically resolve company on LinkedIn, collect employees, draft emails, and optionally open mailto.",
    )
    reach.add_argument(
        "--input-dir",
        default="applications",
        help="Directory containing job packages (default: applications).",
        dest="input_dir",
    )
    reach.add_argument(
        "--storage-state",
        required=True,
        help="Playwright storage_state JSON path (created after you log in manually).",
        dest="storage_state",
    )
    reach.add_argument(
        "--resume-file",
        required=True,
        help="Path to a PDF/DOC resume file for upload.",
        dest="resume_file",
    )
    reach.add_argument(
        "--cover-letter-file",
        help="Optional cover letter file for upload.",
        dest="cover_letter_file",
    )
    reach.add_argument(
        "--output-dir",
        default="reach_out",
        help="Where to write contact/email drafts and screenshots.",
        dest="output_dir",
    )
    reach.add_argument(
        "--max-contacts",
        type=int,
        default=20,
        help="Max employee contacts to collect per company.",
        dest="max_contacts",
    )
    reach.add_argument(
        "--scrape-emails",
        action="store_true",
        help="Best-effort attempt to scrape email addresses from employee profiles.",
        dest="scrape_emails",
    )
    reach.add_argument(
        "--send-emails",
        action="store_true",
        help="If set, opens mail client (mailto) for recipients that have an email address (still confirms per recipient).",
        dest="send_emails",
    )
    reach.add_argument("--name", help="Candidate name.", dest="name")
    reach.add_argument("--headline", help="Candidate headline.", dest="headline")
    reach.add_argument("--skills", help="Override skills string used in messages.", dest="skills")
    reach.add_argument("--subject", help="Email subject override.", dest="subject")
    reach.add_argument("--template", help="Message template override.", dest="template")
    reach.set_defaults(func=cmd_apply_and_reach_out)

    export_auth = subparsers.add_parser(
        "export-auth",
        help="Manually log in and export Playwright storage_state for authenticated automation.",
    )
    export_auth.add_argument(
        "--login-url",
        required=True,
        help="URL to open for manual login (e.g. https://www.linkedin.com/login).",
        dest="login_url",
    )
    export_auth.add_argument(
        "--output-path",
        required=True,
        help="Path to write storage_state JSON (e.g. storage_state.json).",
        dest="output_path",
    )
    export_auth.add_argument(
        "--headless",
        action="store_true",
        help="Run browser headless while exporting auth.",
        dest="headless",
    )
    export_auth.set_defaults(func=cmd_export_auth)

    # --- Autopilot (autonomous bot) ---
    autopilot = subparsers.add_parser(
        "autopilot",
        help="Start the autonomous job scanning, scoring, tailoring, and applying bot.",
    )
    autopilot.add_argument("--query", "-q", default="software engineer", help="Job search query.")
    autopilot.add_argument("--sources", default="remoteok,remotive", help="Comma-separated job sources.")
    autopilot.add_argument("--interval", type=int, default=30, help="Minutes between scan cycles.")
    autopilot.add_argument("--min-score", type=int, default=60, dest="min_score", help="Minimum relevance score (0-100) to proceed.")
    autopilot.add_argument("--max-jobs", type=int, default=20, dest="max_jobs", help="Max jobs per source per scan.")
    autopilot.add_argument("--output-dir", default="autopilot_output", dest="output_dir", help="Output directory for packages and logs.")
    autopilot.add_argument("--candidate-json", "-c", help="Path to JSON candidate profile.")
    autopilot.add_argument("--resume-file", "-r", help="Path to plain-text resume file.", dest="resume_file")
    autopilot.add_argument("--cover-letter-file", help="Optional cover letter file.", dest="cover_letter_file")
    autopilot.add_argument("--name", help="Candidate name.")
    autopilot.add_argument("--email", help="Candidate email.")
    autopilot.add_argument("--headline", help="Candidate headline.")
    autopilot.add_argument("--years-experience", type=int, dest="years_experience", help="Years of experience.")
    autopilot.add_argument("--skills", help="Comma-separated skills.")
    autopilot.add_argument("--portfolio-link", action="append", dest="portfolio_link", help="Portfolio link (repeatable).")
    autopilot.add_argument("--max-packages", type=int, default=50, dest="max_packages", help="Auto-prune oldest packages beyond this limit (default: 50).")
    autopilot.add_argument("--max-log-lines", type=int, default=5000, dest="max_log_lines", help="Rotate log file beyond this many lines (default: 5000).")
    autopilot.add_argument("--storage-state", dest="storage_state", help="Playwright storage_state JSON for LinkedIn auto-apply.")
    autopilot.set_defaults(func=cmd_autopilot)

    return parser


def main(argv: list[str] | None = None) -> None:
    print_banner()
    parser = build_parser()
    args = parser.parse_args(argv)
    args.func(args)


