"""VP deep-learn — start / run / complete task flow."""

from __future__ import annotations

import json
import os
import re
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any

from niannian.virtual_persona.assembly_master import (
    ASSEMBLY_MASTER,
    append_section,
    assembly_master_path,
    merge_deep_learn_results,
)
from niannian.virtual_persona.deep_learn_web import DeepLearnSynthesizeFn, run_task
from niannian.virtual_persona.canon_lookup import resolve_fanwork_policy
from niannian.virtual_persona.pack import read_pack, write_pack
from niannian.virtual_persona.profile_md import parse_profile_sections, section_is_empty
from niannian.virtual_persona.stage import StageTransitionError, get_stage, transition_stage
from niannian.virtual_persona.profile_schema import DEEP_LEARN_PROFILE_SECTIONS

if TYPE_CHECKING:
    from niannian.soul.search_providers import SearchProvider
    from niannian.virtual_persona.store import VirtualPersonaStore

DEEP_LEARN_DIR = "deep_learn"
TASKS_FILE = "tasks.json"
RUNS_DIR = "runs"

# Profile sections distilled in deep-learn (skip addressing).
DEEP_LEARN_TOPICS: tuple[str, ...] = DEEP_LEARN_PROFILE_SECTIONS


class DeepLearnError(ValueError):
    """Illegal deep-learn operation for current VP stage or state."""


class DeepLearnSearchUnavailable(DeepLearnError):
    """Live search requested but no provider is configured."""


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _deep_learn_dir(store: VirtualPersonaStore, vp_id: str) -> Path:
    path = store.vp_dir(vp_id) / DEEP_LEARN_DIR
    path.mkdir(parents=True, exist_ok=True)
    return path


def _runs_dir(store: VirtualPersonaStore, vp_id: str) -> Path:
    path = _deep_learn_dir(store, vp_id) / RUNS_DIR
    path.mkdir(parents=True, exist_ok=True)
    return path


def _tasks_path(store: VirtualPersonaStore, vp_id: str) -> Path:
    return _deep_learn_dir(store, vp_id) / TASKS_FILE


def _slug_task_id(topic: str) -> str:
    slug = re.sub(r"[^\w\-]+", "_", str(topic or "topic").strip())[:48]
    return f"t_{slug or 'topic'}"


def _is_search_unavailable(search: SearchProvider | None) -> bool:
    from niannian.soul.search_providers import StaticSearchProvider

    if search is None:
        return True
    if isinstance(search, StaticSearchProvider) and not search._hits:
        return True
    return False


def is_live_search_configured(yaml_cfg: dict[str, Any]) -> bool:
    """True when web_learning search is anysearch with a non-empty API key."""
    search_cfg = yaml_cfg.get("search") or {}
    provider = str(search_cfg.get("provider") or "mock").lower()
    if provider != "anysearch":
        return False
    api_key_env = str(search_cfg.get("api_key_env") or "ANYSEARCH_API_KEY")
    return bool(os.environ.get(api_key_env, "").strip())


def resolve_deep_learn_mock_default(yaml_cfg: dict[str, Any]) -> bool:
    """Default mock=true unless live search (provider + API key) is configured."""
    return not is_live_search_configured(yaml_cfg)


def deep_learn_search_mode(*, mock: bool) -> str:
    return "mock" if mock else "live"


def _build_tasks_from_profile(store: VirtualPersonaStore, vp_id: str) -> dict[str, Any]:
    row = store.get(vp_id)
    label = str(row.get("label") or vp_id)
    profile_md = str(row.get("profile_md") or "")
    parsed = parse_profile_sections(profile_md)
    title = parsed.pop("__title__", "") or label

    tasks: list[dict[str, Any]] = []
    for topic in DEEP_LEARN_TOPICS:
        excerpt = str(parsed.get(topic) or "")
        tasks.append(
            {
                "task_id": _slug_task_id(topic),
                "topic": topic,
                "section": topic,
                "status": "done" if not section_is_empty(excerpt) else "pending",
                "source_excerpt": excerpt,
            }
        )

    session_id = f"dl_{uuid.uuid4().hex[:12]}"
    return {
        "session_id": session_id,
        "vp_id": vp_id,
        "label": label,
        "title": title,
        "started_at": _now_iso(),
        "tasks": tasks,
    }


def _load_tasks(store: VirtualPersonaStore, vp_id: str) -> dict[str, Any] | None:
    path = _tasks_path(store, vp_id)
    if not path.is_file():
        return None
    with path.open(encoding="utf-8") as f:
        data = json.load(f)
    return data if isinstance(data, dict) else None


def _save_tasks(store: VirtualPersonaStore, vp_id: str, payload: dict[str, Any]) -> None:
    path = _tasks_path(store, vp_id)
    with path.open("w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)
        f.write("\n")


def _find_task(session: dict[str, Any], task_id: str) -> dict[str, Any] | None:
    for task in session.get("tasks") or []:
        if isinstance(task, dict) and str(task.get("task_id")) == task_id:
            return task
    return None


def _merge_runs_to_assembly_master(store: VirtualPersonaStore, vp_id: str) -> str:
    row = store.get(vp_id)
    label = str(row.get("label") or vp_id)
    parsed = parse_profile_sections(str(row.get("profile_md") or ""))
    title = parsed.pop("__title__", "") or label

    runs_dir = _runs_dir(store, vp_id)
    run_sections: dict[str, str] = {}
    if runs_dir.is_dir():
        for run_path in sorted(runs_dir.glob("*.json")):
            try:
                with run_path.open(encoding="utf-8") as f:
                    run = json.load(f)
            except (OSError, json.JSONDecodeError):
                continue
            if not isinstance(run, dict):
                continue
            topic = str(run.get("topic") or run.get("section") or "").strip()
            summary = str(run.get("summary") or run.get("draft_body") or "").strip()
            if topic and summary:
                run_sections[topic] = summary

    section_items: list[dict[str, str]] = []
    for topic in DEEP_LEARN_TOPICS:
        body = run_sections.get(topic)
        if not body:
            body = str(parsed.get(topic) or "").strip()
        if not body:
            body = f"（{topic} · 待日常积累）"
        section_items.append({"heading": topic, "body": body})

    return merge_deep_learn_results(
        store,
        vp_id,
        section_items,
        title=title,
        order=list(DEEP_LEARN_TOPICS),
    )


def start_deep_learn(store: VirtualPersonaStore, vp_id: str) -> dict[str, Any]:
    current = get_stage(store, vp_id)
    if current == "customize":
        try:
            transition_stage(store, vp_id, "deep_learn")
        except StageTransitionError as exc:
            raise DeepLearnError(str(exc)) from exc
    elif current != "deep_learn":
        raise DeepLearnError(f"deep-learn/start requires customize or deep_learn stage, got {current!r}")

    session = _build_tasks_from_profile(store, vp_id)
    _save_tasks(store, vp_id, session)
    return {
        "vp_id": vp_id,
        "stage": get_stage(store, vp_id),
        "session_id": session["session_id"],
        "tasks": list(session["tasks"]),
    }


def run_deep_learn(
    store: VirtualPersonaStore,
    vp_id: str,
    *,
    task_id: str,
    search: SearchProvider | None = None,
    allowlist: list[str] | None = None,
    synthesize_fn: DeepLearnSynthesizeFn | None = None,
    mock: bool = True,
    max_results: int = 5,
) -> dict[str, Any]:
    if get_stage(store, vp_id) != "deep_learn":
        raise DeepLearnError(f"deep-learn/run requires stage deep_learn, got {get_stage(store, vp_id)!r}")

    if not mock and _is_search_unavailable(search):
        raise DeepLearnSearchUnavailable("search provider not configured for live deep-learn")

    session = _load_tasks(store, vp_id)
    if session is None:
        raise DeepLearnError("deep-learn tasks not found; call start first")

    task = _find_task(session, task_id)
    if task is None:
        raise DeepLearnError(f"unknown task_id: {task_id!r}")

    row = store.get(vp_id)
    label = str(row.get("label") or vp_id)
    topic = str(task.get("topic") or task.get("section") or "")
    excerpt = str(task.get("source_excerpt") or "")
    fanwork_policy = resolve_fanwork_policy(store, vp_id)

    run_id = f"run_{uuid.uuid4().hex[:12]}"
    started_at = _now_iso()
    sources_dir = store.vp_dir(vp_id) / "sources"

    task_result = run_task(
        label=label,
        topic=topic,
        excerpt=excerpt,
        run_id=run_id,
        sources_dir=sources_dir,
        search=search,
        allowlist=allowlist,
        fanwork_policy=fanwork_policy,
        mock=mock,
        max_results=max_results,
        synthesize_fn=synthesize_fn,
    )
    summary = str(task_result.get("summary") or "").strip()

    run_record: dict[str, Any] = {
        "run_id": run_id,
        "task_id": task_id,
        "topic": topic,
        "section": topic,
        "started_at": started_at,
        "completed_at": _now_iso(),
        "query": task_result.get("query"),
        "hits_count": task_result.get("hits_count", 0),
        "source_urls": list(task_result.get("source_urls") or []),
        "fanwork_policy": task_result.get("fanwork_policy"),
        "summary": summary,
        "draft_body": summary,
        "mock": bool(mock),
    }
    run_path = _runs_dir(store, vp_id) / f"{run_id}.json"
    with run_path.open("w", encoding="utf-8") as f:
        json.dump(run_record, f, ensure_ascii=False, indent=2)
        f.write("\n")

    task["status"] = "done"
    task["last_run_id"] = run_id
    _save_tasks(store, vp_id, session)
    append_section(store, vp_id, topic, summary)

    return {
        "vp_id": vp_id,
        "stage": get_stage(store, vp_id),
        "run_id": run_id,
        "task_id": task_id,
        "topic": topic,
        "query": run_record["query"],
        "hits_count": run_record["hits_count"],
        "source_urls": run_record["source_urls"],
        "fanwork_policy": run_record["fanwork_policy"],
        "summary": summary,
        "sources_written": len(task_result.get("source_paths") or []),
        "search_mode": deep_learn_search_mode(mock=mock),
        "run_path": str(run_path),
    }


def run_all_deep_learn(
    store: VirtualPersonaStore,
    vp_id: str,
    *,
    search: SearchProvider | None = None,
    allowlist: list[str] | None = None,
    synthesize_fn: DeepLearnSynthesizeFn | None = None,
    mock: bool = True,
    max_results: int = 5,
) -> dict[str, Any]:
    if get_stage(store, vp_id) != "deep_learn":
        raise DeepLearnError(
            f"deep-learn/run-all requires stage deep_learn, got {get_stage(store, vp_id)!r}"
        )

    if not mock and _is_search_unavailable(search):
        raise DeepLearnSearchUnavailable("search provider not configured for live deep-learn")

    session = _load_tasks(store, vp_id)
    if session is None:
        raise DeepLearnError("deep-learn tasks not found; call start first")

    pending = [
        task
        for task in session.get("tasks") or []
        if isinstance(task, dict) and str(task.get("status")) == "pending"
    ]

    completed = 0
    failed = 0
    task_results: list[dict[str, Any]] = []

    for task in pending:
        task_id = str(task.get("task_id") or "")
        try:
            result = run_deep_learn(
                store,
                vp_id,
                task_id=task_id,
                search=search,
                allowlist=allowlist,
                synthesize_fn=synthesize_fn,
                mock=mock,
                max_results=max_results,
            )
            completed += 1
            task_results.append(
                {
                    "task_id": task_id,
                    "run_id": result["run_id"],
                    "ok": True,
                }
            )
        except (DeepLearnError, DeepLearnSearchUnavailable):
            failed += 1
            task_results.append(
                {
                    "task_id": task_id,
                    "run_id": None,
                    "ok": False,
                }
            )

    return {
        "completed": completed,
        "failed": failed,
        "tasks": task_results,
        "search_mode": deep_learn_search_mode(mock=mock),
    }


def complete_deep_learn(store: VirtualPersonaStore, vp_id: str) -> dict[str, Any]:
    if get_stage(store, vp_id) != "deep_learn":
        raise DeepLearnError(
            f"deep-learn/complete requires stage deep_learn, got {get_stage(store, vp_id)!r}"
        )

    assembly_md = _merge_runs_to_assembly_master(store, vp_id)
    try:
        result = transition_stage(store, vp_id, "daily")
    except StageTransitionError as exc:
        raise DeepLearnError(str(exc)) from exc

    pack = read_pack(store, vp_id)
    now = _now_iso()
    pack["bootstrap_complete"] = True
    pack["deep_learn_completed_at"] = now
    pack["updated_at"] = now
    write_pack(store, vp_id, pack)

    return {
        "vp_id": vp_id,
        "previous_stage": result["previous_stage"],
        "stage": result["stage"],
        "assembly_master_path": str(assembly_master_path(store, vp_id)),
        "assembly_master_bytes": len(assembly_md.encode("utf-8")),
        "bootstrap_complete": True,
    }
