from __future__ import annotations

import json
import shutil
import signal
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional, Set

from .models import (
    ApplicationLog,
    BotConfig,
    CandidateProfile,
    JobDescription,
)
from .orchestrator import JobSearchOrchestrator
from .scoring import RelevanceScorer
from .sources import RemoteOKSource, RemotiveSource, WeWorkRemotelySource
from .terminal_ui import console, header, section


class JobBot:
    """
    Autonomous job scanning, scoring, tailoring, and applying daemon.
    """

    def __init__(
        self,
        candidate: CandidateProfile,
        config: BotConfig,
        *,
        resume_file: Optional[Path] = None,
        cover_letter_file: Optional[Path] = None,
        storage_state: Optional[Path] = None,
    ) -> None:
        self.candidate = candidate
        self.config = config
        self.resume_file = resume_file
        self.cover_letter_file = cover_letter_file
        self.storage_state = storage_state

        self.orchestrator = JobSearchOrchestrator()
        self.scorer = RelevanceScorer()

        self.output_dir = Path(config.output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)

        self._applied_path = self.output_dir / "applied_jobs.json"
        self._log_path = self.output_dir / "application_log.jsonl"
        self._applied_urls: Set[str] = self._load_applied_urls()

        self._running = True
        signal.signal(signal.SIGINT, self._shutdown)
        signal.signal(signal.SIGTERM, self._shutdown)

    # --- Lifecycle ---

    def _shutdown(self, signum, frame) -> None:
        console.print("\n[bold yellow]Received shutdown signal. Finishing current task...[/]")
        self._running = False

    def run(self) -> None:
        """Main daemon loop."""
        header("Jobberman Autopilot Started")
        console.print(f"[dim]Query: {self.config.query} | Sources: {', '.join(self.config.sources)} | "
                       f"Interval: {self.config.interval_minutes}m | Min Score: {self.config.min_score} | "
                       f"Max Packages: {self.config.max_packages}[/]")
        console.print()

        cycle = 0
        while self._running:
            cycle += 1
            section(f"Scan Cycle #{cycle}")
            try:
                self._run_cycle()
            except Exception as e:
                console.print(f"[bold red]Cycle error: {e}[/]")
                self._write_log(ApplicationLog(
                    action="ERROR", job_title="", status="CYCLE_ERROR", details=str(e)
                ))

            # Storage maintenance after each cycle
            self._prune_packages()
            self._rotate_log()

            if not self._running:
                break

            console.print(f"\n[dim]Sleeping {self.config.interval_minutes} minutes until next scan...[/]")
            self._interruptible_sleep(self.config.interval_minutes * 60)

        header("Jobberman Autopilot Stopped")

    # --- Core Cycle ---

    def _run_cycle(self) -> None:
        jobs = self._scan_all_sources()
        if not jobs:
            console.print("[yellow]No new jobs found this cycle.[/]")
            return

        new_jobs = [j for j in jobs if (j.url or "") not in self._applied_urls]
        console.print(f"[green]Found {len(jobs)} jobs, {len(new_jobs)} new (not previously processed).[/]")

        for job in new_jobs:
            if not self._running:
                break
            self._process_job(job)

    def _process_job(self, job: JobDescription) -> None:
        job_url = job.url or ""
        job_title = job.title or "Unknown"
        job_company = job.company or "Unknown"

        # Step 1: Score
        try:
            with console.status(f"[bold green]Scoring: {job_title}...[/]"):
                match = self.scorer.score(self.candidate, job)
        except Exception as e:
            console.print(f"[red]  Scoring failed for {job_title}: {e}[/]")
            self._write_log(ApplicationLog(
                action="ERROR", job_title=job_title, company=job_company,
                url=job_url, status="SCORE_FAILED", details=str(e)
            ))
            self._mark_applied(job_url)
            return

        console.print(f"  Score: [bold]{match.score}/100[/] — {match.reasoning}")
        self._write_log(ApplicationLog(
            action="SCORED", job_title=job_title, company=job_company,
            url=job_url, score=match.score, status="OK", details=match.reasoning
        ))

        if match.score < self.config.min_score:
            console.print(f"  [yellow]Skipped (below threshold {self.config.min_score}).[/]")
            self._write_log(ApplicationLog(
                action="SKIPPED", job_title=job_title, company=job_company,
                url=job_url, score=match.score, status="SKIPPED_LOW_SCORE"
            ))
            self._mark_applied(job_url)
            return

        # Step 2: Tailor
        try:
            with console.status(f"[bold green]Tailoring resume for: {job_title}...[/]"):
                result = self.orchestrator.full_pipeline_for_job(job, self.candidate)
        except Exception as e:
            console.print(f"[red]  Tailoring failed for {job_title}: {e}[/]")
            self._write_log(ApplicationLog(
                action="ERROR", job_title=job_title, company=job_company,
                url=job_url, score=match.score, status="TAILOR_FAILED", details=str(e)
            ))
            self._mark_applied(job_url)
            return

        self._write_log(ApplicationLog(
            action="TAILORED", job_title=job_title, company=job_company,
            url=job_url, score=match.score, status="OK"
        ))

        # Step 3: Save tailored output (single consolidated JSON file)
        self._save_package(job, result, match.score)

        # Step 4: Auto-apply (LinkedIn only, if storage_state provided)
        applied = False
        if self.storage_state and job_url and "linkedin.com/jobs" in job_url and self.resume_file:
            try:
                applied = self._auto_apply_linkedin(job_url)
            except Exception as e:
                console.print(f"[red]  Auto-apply failed: {e}[/]")
                self._write_log(ApplicationLog(
                    action="ERROR", job_title=job_title, company=job_company,
                    url=job_url, score=match.score, status="APPLY_FAILED", details=str(e)
                ))

        status = "SUBMITTED" if applied else "PACKAGED_FOR_MANUAL_APPLY"
        console.print(f"  [{'green' if applied else 'cyan'}]Result: {status}[/]")
        self._write_log(ApplicationLog(
            action="APPLIED" if applied else "PACKAGED",
            job_title=job_title, company=job_company,
            url=job_url, score=match.score, status=status
        ))

        self._mark_applied(job_url)

    # --- Source Scanning ---

    def _scan_all_sources(self) -> List[JobDescription]:
        source_map = {
            "remoteok": RemoteOKSource,
            "remotive": RemotiveSource,
            "weworkremotely": WeWorkRemotelySource,
        }
        all_jobs: List[JobDescription] = []

        for source_name in self.config.sources:
            source_cls = source_map.get(source_name.lower())
            if not source_cls:
                console.print(f"[yellow]Unknown source: {source_name}, skipping.[/]")
                continue
            try:
                with console.status(f"[bold green]Scanning {source_name}...[/]"):
                    source = source_cls()
                    jobs = source.search(query=self.config.query, max_jobs=self.config.max_jobs_per_scan)
                console.print(f"  [green]{source_name}: {len(jobs)} listings[/]")
                all_jobs.extend(jobs)
            except Exception as e:
                console.print(f"  [red]{source_name} failed: {e}[/]")

        return all_jobs

    # --- LinkedIn Auto-Apply ---

    def _auto_apply_linkedin(self, job_url: str) -> bool:
        from .automation import LinkedInEasyApplyApplier, PlaywrightSession

        session = PlaywrightSession(storage_state=self.storage_state)
        screenshots_dir = self.output_dir / "screenshots"

        applier = LinkedInEasyApplyApplier(
            resume_file=self.resume_file,
            cover_letter_file=self.cover_letter_file,
            screenshots_dir=screenshots_dir,
        )

        result_holder = {"submitted": False}

        def apply_fn(page) -> None:
            r = applier.apply(
                page=page,
                job_url=job_url,
                confirm_before_final_submit=lambda _: True,  # Auto-confirm in autopilot mode
            )
            result_holder["submitted"] = r.submitted

        session.run(job_fn=apply_fn)
        return result_holder["submitted"]

    # --- Persistence (Consolidated) ---

    def _save_package(self, job: JobDescription, result: dict, score: int) -> None:
        """Save a single consolidated JSON file per job instead of multiple text files."""
        import re
        slug = re.sub(r"[^a-z0-9]+", "-", (job.title or "job").lower()).strip("-")[:60]
        ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
        filename = f"{ts}_{slug}.json"

        packages_dir = self.output_dir / "packages"
        packages_dir.mkdir(parents=True, exist_ok=True)

        tr = result.get("tailored_resume")
        tc = result.get("tailored_cover_letter")

        package = {
            "created_at": ts,
            "score": score,
            "job": {
                "title": job.title or "",
                "company": job.company or "",
                "url": job.url or "",
                "location": job.location or "",
                "description": job.raw_text[:2000],  # Cap description to save space
            },
            "tailored_resume": {
                "summary": getattr(tr, "summary", "") if tr else "",
                "highlighted_skills": getattr(tr, "highlighted_skills", []) if tr else [],
                "bullet_points": getattr(tr, "bullet_points", []) if tr else [],
            },
            "cover_letter": {
                "intro": getattr(tc, "intro", "") if tc else "",
                "body": getattr(tc, "body", "") if tc else "",
                "closing": getattr(tc, "closing", "") if tc else "",
            },
        }

        (packages_dir / filename).write_text(json.dumps(package, indent=2))

    def _load_applied_urls(self) -> Set[str]:
        if self._applied_path.exists():
            try:
                data = json.loads(self._applied_path.read_text())
                return set(data.get("applied_urls", []))
            except Exception:
                return set()
        return set()

    def _mark_applied(self, url: str) -> None:
        if url:
            self._applied_urls.add(url)
        data = {"applied_urls": sorted(self._applied_urls)}
        self._applied_path.write_text(json.dumps(data, indent=2))

    def _write_log(self, entry: ApplicationLog) -> None:
        with self._log_path.open("a") as f:
            f.write(entry.model_dump_json() + "\n")

    # --- Storage Management ---

    def _prune_packages(self) -> None:
        """Remove oldest package files when exceeding max_packages limit."""
        packages_dir = self.output_dir / "packages"
        if not packages_dir.exists():
            return

        # Collect both files (.json) and legacy directories
        items = sorted(packages_dir.iterdir(), key=lambda p: p.stat().st_mtime)
        overflow = len(items) - self.config.max_packages

        if overflow <= 0:
            return

        for item in items[:overflow]:
            try:
                if item.is_dir():
                    shutil.rmtree(item)
                else:
                    item.unlink()
            except Exception:
                pass

        console.print(f"[dim]Pruned {overflow} old package(s) (limit: {self.config.max_packages}).[/]")

    def _rotate_log(self) -> None:
        """Rotate application_log.jsonl when it exceeds max_log_lines."""
        if not self._log_path.exists():
            return

        try:
            lines = self._log_path.read_text().strip().split("\n")
        except Exception:
            return

        if len(lines) <= self.config.max_log_lines:
            return

        # Keep the most recent entries, archive the rest
        archive_path = self._log_path.with_suffix(".old.jsonl")
        keep = lines[-self.config.max_log_lines:]
        archived = lines[:-self.config.max_log_lines]

        # Append to archive
        with archive_path.open("a") as f:
            for line in archived:
                f.write(line + "\n")

        # Rewrite main log with only recent entries
        self._log_path.write_text("\n".join(keep) + "\n")
        console.print(f"[dim]Rotated log: archived {len(archived)} old entries.[/]")

    def _interruptible_sleep(self, seconds: int) -> None:
        """Sleep in 1-second intervals so we can respond to shutdown quickly."""
        for _ in range(seconds):
            if not self._running:
                break
            time.sleep(1)
