"""Observatory — orchestrates periodic update cycles across sources."""

from __future__ import annotations

import json
from datetime import datetime, timezone

from rich.console import Console
from rich.progress import (
    BarColumn,
    MofNCompleteColumn,
    Progress,
    SpinnerColumn,
    TextColumn,
)

from .config import Config
from .db import Database
from .models import Draft
from .sources import get_fetcher
from .sources.base import SourceDocument

console = Console()


def _doc_to_draft(doc: SourceDocument) -> Draft:
    """Convert a SourceDocument to a Draft for DB storage."""
    extra = doc.extra or {}
    return Draft(
        name=doc.name,
        rev=extra.get("rev", "00"),
        title=doc.title,
        abstract=doc.abstract,
        time=doc.time,
        dt_id=int(doc.source_id) if doc.source_id and doc.source_id.isdigit() else None,
        pages=extra.get("pages"),
        words=extra.get("words"),
        group=extra.get("group"),
        group_uri=extra.get("group_uri"),
        expires=extra.get("expires"),
        ad=extra.get("ad"),
        shepherd=extra.get("shepherd"),
        states=extra.get("states", []),
        full_text=doc.full_text,
        fetched_at=datetime.now(timezone.utc).isoformat(),
        source=doc.source,
        source_id=doc.source_id,
        source_url=doc.source_url,
        doc_status=doc.doc_status,
    )


class Observatory:
    """Orchestrates the full observatory update cycle."""

    def __init__(
        self,
        config: Config | None = None,
        db: Database | None = None,
        analyzer=None,
    ):
        self.config = config or Config.load()
        self.db = db or Database(self.config)
        self._analyzer = analyzer

    @property
    def analyzer(self):
        """Lazy-load analyzer to avoid Anthropic key requirement for status/diff."""
        if self._analyzer is None:
            from .analyzer import Analyzer

            self._analyzer = Analyzer(self.config, self.db)
        return self._analyzer

    def update(
        self,
        sources: list[str] | None = None,
        full: bool = False,
        dry_run: bool = False,
    ) -> dict:
        """Full update cycle.

        1. Snapshot current state
        2. Fetch from enabled sources (delta by default)
        3. Analyze unrated docs (Claude, with caching)
        4. Embed missing docs (Ollama)
        5. Extract ideas from new docs
        6. Re-run gap analysis if >= 5 new docs
        7. Record gap changes in gap_history
        8. Return summary stats

        If dry_run=True, show what would be fetched/analyzed without doing it.
        """
        sources = sources or self.config.observatory_sources
        stats: dict = {"sources": {}, "new_docs": 0, "analyzed": 0, "embedded": 0, "ideas": 0, "gaps_changed": False}

        if dry_run:
            console.print("[bold yellow]DRY RUN[/] — showing what would happen without making changes\n")
            for src_name in sources:
                console.print(f"  Would fetch from: [cyan]{src_name}[/]")
                src = self.db.get_source(src_name)
                if src and src.get("last_fetch"):
                    console.print(f"    Last fetch: {src['last_fetch'][:10]}")
                else:
                    console.print(f"    Last fetch: never (full fetch)")
            unrated = len(self.db.unrated_drafts(limit=10000))
            unembedded = len(self.db.drafts_without_embeddings(limit=10000))
            no_ideas = len(self.db.drafts_without_ideas(limit=10000))
            console.print(f"\n  Would analyze: [bold]{unrated}[/] unrated documents")
            console.print(f"  Would embed: [bold]{unembedded}[/] documents")
            console.print(f"  Would extract ideas from: [bold]{no_ideas}[/] documents")
            stats["dry_run"] = True
            return stats

        # 1. Snapshot current state
        console.print("[bold]1/7[/] Creating snapshot...")
        snapshot_id = self.db.create_snapshot()

        # 2. Fetch from enabled sources
        console.print("[bold]2/7[/] Fetching from sources...")
        total_new = 0
        for src_name in sources:
            try:
                new_count = self._fetch_source(src_name, full=full)
                stats["sources"][src_name] = new_count
                total_new += new_count
            except Exception as e:
                console.print(f"  [red]Error fetching {src_name}: {e}[/]")
                stats["sources"][src_name] = {"error": str(e)}
        stats["new_docs"] = total_new
        console.print(f"  Fetched [bold green]{total_new}[/] new documents total")

        # 3. Analyze unrated docs
        console.print("[bold]3/7[/] Analyzing unrated documents...")
        try:
            analyzed = self.analyzer.rate_all_unrated(limit=200, batch_size=5)
            stats["analyzed"] = analyzed
        except Exception as e:
            console.print(f"  [red]Analysis failed: {e}[/]")
            stats["analyzed"] = 0
            stats["errors"] = stats.get("errors", []) + [f"analyze: {e}"]

        # 4. Embed missing docs
        console.print("[bold]4/7[/] Embedding missing documents...")
        try:
            embedded = self._embed_missing()
            stats["embedded"] = embedded
        except Exception as e:
            console.print(f"  [red]Embedding failed: {e}[/]")
            stats["embedded"] = 0
            stats["errors"] = stats.get("errors", []) + [f"embed: {e}"]

        # 5. Extract ideas from new docs
        console.print("[bold]5/7[/] Extracting ideas...")
        try:
            ideas = self.analyzer.extract_all_ideas(limit=200, batch_size=5, cheap=True)
            stats["ideas"] = ideas
        except Exception as e:
            console.print(f"  [red]Idea extraction failed: {e}[/]")
            stats["ideas"] = 0
            stats["errors"] = stats.get("errors", []) + [f"ideas: {e}"]

        # 6. Re-run gap analysis if enough new docs
        if total_new >= 5:
            console.print("[bold]6/7[/] Re-running gap analysis...")
            try:
                gaps = self.analyzer.gap_analysis()
                if gaps:
                    self.db.record_gap_history(snapshot_id, gaps)
                    stats["gaps_changed"] = True
                    console.print(f"  Found [bold]{len(gaps)}[/] gaps")
            except Exception as e:
                console.print(f"  [red]Gap analysis failed: {e}[/]")
                stats["errors"] = stats.get("errors", []) + [f"gaps: {e}"]
        else:
            console.print(f"[bold]6/7[/] Skipping gap analysis ({total_new} < 5 new docs)")
            # Record current gaps unchanged
            current_gaps = self.db.all_gaps()
            if current_gaps:
                self.db.record_gap_history(snapshot_id, current_gaps)

        # 7. Update source records
        console.print("[bold]7/7[/] Updating source records...")
        for src_name in sources:
            count = len(self.db.drafts_by_source(src_name, limit=10000))
            self.db.upsert_source(src_name, doc_count=count)

        console.print("\n[bold green]Observatory update complete![/]")
        console.print(f"  New docs: {total_new}  |  Analyzed: {stats['analyzed']}  |  Embedded: {stats['embedded']}  |  Ideas: {stats['ideas']}")
        return stats

    def _fetch_source(self, source_name: str, full: bool = False) -> int:
        """Fetch documents from a single source. Returns count of new docs."""
        fetcher = get_fetcher(source_name, self.config)
        try:
            # Delta fetch: only since last fetch unless full=True
            since = None
            if not full:
                src = self.db.get_source(source_name)
                if src and src.get("last_fetch"):
                    since = src["last_fetch"][:10]  # Date portion only

            docs = fetcher.search(self.config.search_keywords, since=since)

            new_count = 0
            for doc in docs:
                existing = self.db.get_draft(doc.name)
                if existing is None:
                    new_count += 1
                draft = _doc_to_draft(doc)
                self.db.upsert_draft(draft)

            # Download text for docs missing it
            missing_text = [
                d for d in docs
                if self.db.get_draft(d.name) and self.db.get_draft(d.name).full_text is None
            ]
            if missing_text:
                console.print(f"  Downloading text for {len(missing_text)} {source_name} docs...")
                with Progress(
                    SpinnerColumn(),
                    TextColumn("[progress.description]{task.description}"),
                    BarColumn(),
                    MofNCompleteColumn(),
                    console=console,
                ) as progress:
                    task = progress.add_task(f"Downloading {source_name} texts...", total=len(missing_text))
                    for doc in missing_text:
                        text = fetcher.download_text(doc)
                        if text:
                            draft = self.db.get_draft(doc.name)
                            if draft:
                                draft.full_text = text
                                self.db.upsert_draft(draft)
                        progress.advance(task)

            return new_count
        finally:
            fetcher.close()

    def _embed_missing(self) -> int:
        """Embed documents that don't have embeddings yet."""
        missing = self.db.drafts_without_embeddings(limit=500)
        if not missing:
            console.print("  All documents already embedded.")
            return 0

        try:
            from .embeddings import Embedder

            embedder = Embedder(self.config, self.db)
        except Exception as e:
            console.print(f"  [yellow]Skipping embeddings (Ollama unavailable): {e}[/]")
            return 0

        count = 0
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            BarColumn(),
            MofNCompleteColumn(),
            console=console,
        ) as progress:
            task = progress.add_task("Embedding...", total=len(missing))
            for name in missing:
                try:
                    vec = embedder.embed_draft(name)
                    if vec is not None:
                        count += 1
                except Exception:
                    pass
                progress.advance(task)

        console.print(f"  Embedded [bold green]{count}[/] documents")
        return count

    def status(self) -> dict:
        """Current observatory state -- doc counts, sources, last update."""
        total = self.db.count_drafts()
        sources = self.db.all_sources()
        snapshots = self.db.get_snapshots(limit=1)
        gaps = self.db.all_gaps()

        # Count by source
        source_counts = {}
        for src in sources:
            source_counts[src["name"]] = src["doc_count"]

        # Unrated / unembedded
        unrated = len(self.db.unrated_drafts(limit=10000))
        unembedded = len(self.db.drafts_without_embeddings(limit=10000))

        last_update = snapshots[0]["snapshot_at"] if snapshots else None

        return {
            "total_docs": total,
            "sources": source_counts,
            "unrated": unrated,
            "unembedded": unembedded,
            "gaps": len(gaps),
            "last_update": last_update,
            "snapshots": len(self.db.get_snapshots(limit=100)),
        }

    def diff(self, since: str | None = None) -> dict:
        """What changed since a date -- new docs, gap changes."""
        if since is None:
            # Default to last snapshot
            snapshots = self.db.get_snapshots(limit=2)
            if len(snapshots) >= 2:
                since = snapshots[1]["snapshot_at"]
            else:
                since = "2000-01-01"

        # New docs since date
        new_docs = self.db.conn.execute(
            "SELECT name, title, source, time FROM drafts WHERE fetched_at > ? ORDER BY time DESC",
            (since,),
        ).fetchall()

        # Gap changes
        gap_timeline = self.db.gap_history_timeline()
        recent_gaps = [g for g in gap_timeline if g.get("recorded_at", "") > since]

        return {
            "since": since,
            "new_docs": [dict(r) for r in new_docs],
            "new_doc_count": len(new_docs),
            "gap_changes": recent_gaps,
        }
