"""Research run state: source registry + counters tied to scratch path."""
from __future__ import annotations

import json
import os
import re
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any
from urllib.parse import urlparse

from utils.path_safety import get_user_output_dir, safe_resolve_path


def normalize_source_url(url: str) -> str:
    """Normalize URL for dedupe (drop fragment, lowercase scheme/host)."""
    if not isinstance(url, str):
        return ""
    raw = url.strip()
    if not raw:
        return ""
    try:
        p = urlparse(raw)
        if not p.scheme or not p.netloc:
            return raw
        scheme = p.scheme.lower()
        netloc = p.netloc.lower()
        path = p.path or ""
        query = f"?{p.query}" if p.query else ""
        return f"{scheme}://{netloc}{path}{query}"
    except Exception:
        return raw


def _state_path_from_scratch_path(scratch_path: str) -> str:
    base, ext = os.path.splitext(scratch_path)
    if ext.lower() == ".md":
        return base + "_state.json"
    return scratch_path + "_state.json"


def resolve_scratch_path(path: str, *, allow_write: bool = False) -> str | None:
    """Resolve a scratch path safely under user output dir."""
    resolved, err = safe_resolve_path(
        get_user_output_dir(),
        (path or "").strip(),
        allow_write=allow_write,
        create_parents=allow_write,
    )
    if err or not resolved:
        return None
    return resolved


def resolve_state_path_from_scratch(scratch_path: str, *, allow_write: bool = False) -> str | None:
    """Resolve state JSON path for a given scratch file path."""
    scratch_resolved = resolve_scratch_path(scratch_path, allow_write=allow_write)
    if not scratch_resolved:
        return None
    candidate = _state_path_from_scratch_path(scratch_resolved)
    state_resolved, err = safe_resolve_path(
        get_user_output_dir(),
        candidate,
        allow_write=allow_write,
        create_parents=allow_write,
    )
    if err or not state_resolved:
        return None
    return state_resolved


@dataclass
class ResearchRunState:
    """Thread/run-level state persisted next to the research scratch file."""

    scratch_path: str = ""
    state_path: str = ""
    queries_run: list[str] = field(default_factory=list)
    urls_registered: list[dict[str, Any]] = field(default_factory=list)
    search_count: int = 0
    read_count: int = 0
    append_count: int = 0
    last_synthesis_recommendation: str = ""
    last_phase: str = ""
    sub_questions: list[str] = field(default_factory=list)
    covered_sub_questions: list[str] = field(default_factory=list)
    updated_at: str = ""

    def to_dict(self) -> dict[str, Any]:
        d = asdict(self)
        d["updated_at"] = datetime.now().isoformat(timespec="seconds")
        return d


def empty_state() -> ResearchRunState:
    return ResearchRunState()


def load_state_for_scratch(scratch_path: str) -> ResearchRunState:
    """Load persisted state for scratch_path, or return empty bound state."""
    scratch_resolved = resolve_scratch_path(scratch_path, allow_write=False)
    if not scratch_resolved:
        return empty_state()
    state_path = resolve_state_path_from_scratch(scratch_resolved, allow_write=False)
    if not state_path:
        return ResearchRunState(scratch_path=scratch_resolved)
    if not os.path.isfile(state_path):
        return ResearchRunState(scratch_path=scratch_resolved, state_path=state_path)
    try:
        with open(state_path, "r", encoding="utf-8") as f:
            payload = json.load(f)
        if not isinstance(payload, dict):
            return ResearchRunState(scratch_path=scratch_resolved, state_path=state_path)
        return ResearchRunState(
            scratch_path=str(payload.get("scratch_path") or scratch_resolved),
            state_path=str(payload.get("state_path") or state_path),
            queries_run=[str(x) for x in (payload.get("queries_run") or []) if str(x).strip()],
            urls_registered=[x for x in (payload.get("urls_registered") or []) if isinstance(x, dict)],
            search_count=int(payload.get("search_count") or 0),
            read_count=int(payload.get("read_count") or 0),
            append_count=int(payload.get("append_count") or 0),
            last_synthesis_recommendation=str(payload.get("last_synthesis_recommendation") or ""),
            last_phase=str(payload.get("last_phase") or ""),
            sub_questions=[str(x) for x in (payload.get("sub_questions") or []) if str(x).strip()],
            covered_sub_questions=[str(x) for x in (payload.get("covered_sub_questions") or []) if str(x).strip()],
            updated_at=str(payload.get("updated_at") or ""),
        )
    except Exception:
        return ResearchRunState(scratch_path=scratch_resolved, state_path=state_path)


def bind_state_to_scratch(state: ResearchRunState, scratch_path: str) -> None:
    scratch_resolved = resolve_scratch_path(scratch_path, allow_write=True)
    if not scratch_resolved:
        return
    state.scratch_path = scratch_resolved
    state_path = resolve_state_path_from_scratch(scratch_resolved, allow_write=True)
    if state_path:
        state.state_path = state_path


def save_state(state: ResearchRunState) -> bool:
    if not state.scratch_path:
        return False
    if not state.state_path:
        state_path = resolve_state_path_from_scratch(state.scratch_path, allow_write=True)
        if not state_path:
            return False
        state.state_path = state_path
    try:
        with open(state.state_path, "w", encoding="utf-8") as f:
            json.dump(state.to_dict(), f, indent=2, ensure_ascii=True)
        return True
    except Exception:
        return False


def merge_url(
    state: ResearchRunState,
    *,
    url: str,
    title: str = "",
    source_tool: str = "",
    credibility_label: str = "",
    credibility_total: int | None = None,
) -> None:
    norm = normalize_source_url(url)
    if not norm.startswith("http"):
        return
    domain = ""
    try:
        domain = (urlparse(norm).netloc or "").lstrip("www.")
    except Exception:
        domain = ""
    now = datetime.now().isoformat(timespec="seconds")
    for row in state.urls_registered:
        if normalize_source_url(str(row.get("url") or "")) == norm:
            row["title"] = str(title or row.get("title") or "")
            row["domain"] = domain or str(row.get("domain") or "")
            row["last_seen_at"] = now
            if source_tool:
                row["source_tool"] = source_tool
            if credibility_label:
                row["credibility_label"] = credibility_label
            if credibility_total is not None:
                row["credibility_total"] = int(credibility_total)
            return
    entry: dict[str, Any] = {
        "url": norm,
        "title": title or domain or norm,
        "domain": domain,
        "first_seen_at": now,
        "last_seen_at": now,
        "source_tool": source_tool or "",
    }
    if credibility_label:
        entry["credibility_label"] = credibility_label
    if credibility_total is not None:
        entry["credibility_total"] = int(credibility_total)
    state.urls_registered.append(entry)


def parse_urls_from_deep_search_output(text: str) -> list[dict[str, str]]:
    """Extract URLs from deep_web_search markdown/json output."""
    if not isinstance(text, str) or not text.strip():
        return []
    results: list[dict[str, str]] = []
    # Prefer JSON block when present.
    jm = re.search(r"```json\s*([\s\S]*?)```", text, flags=re.IGNORECASE)
    if jm:
        try:
            payload = json.loads(jm.group(1).strip())
            if isinstance(payload, list):
                for item in payload:
                    if not isinstance(item, dict):
                        continue
                    url = str(item.get("url") or "").strip()
                    if not url:
                        continue
                    title = str(item.get("title") or "").strip()
                    results.append({"url": url, "title": title})
                if results:
                    return results
        except Exception:
            pass
    for m in re.finditer(r"(https?://[^\s)]+)", text):
        url = m.group(1).strip().rstrip(".,")
        if url:
            results.append({"url": url, "title": ""})
    # Dedup by normalized URL order-preserving.
    out: list[dict[str, str]] = []
    seen: set[str] = set()
    for row in results:
        n = normalize_source_url(row.get("url", ""))
        if not n or n in seen:
            continue
        seen.add(n)
        out.append({"url": n, "title": row.get("title", "")})
    return out


def parse_credibility_from_analysis(text: str) -> tuple[str, int | None]:
    if not isinstance(text, str):
        return "", None
    m = re.search(r"\b(High|Medium|Low)\s*\((\d+)/9\)", text, re.IGNORECASE)
    if not m:
        return "", None
    label = m.group(1).capitalize()
    try:
        total = int(m.group(2))
    except Exception:
        total = None
    return label, total
