from __future__ import annotations

import re
import subprocess
import sys
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Literal, TypedDict

from .opus import call_opus
from .prompts import Period, build_gap_analysis_prompt, build_research_prompt, build_synthesis_prompt

NOTES_DIR = Path.home() / "notes"
AI_LANDSCAPE_PATH = NOTES_DIR / "AI Landscape.md"
AI_NEWS_LOG_PATH = NOTES_DIR / "AI News Log.md"
WEEKLY_MARKER = "<!-- Weekly entries below, most recent first -->"
CURRENT_LANDSCAPE_HEADER = "## Current Landscape"
MONTHLY_HEADER = "## Monthly Reviews"
QUARTERLY_HEADER = "## Quarterly Reviews"
YEARLY_HEADER = "## Annual Reviews"


class PipelineState(TypedDict):
    period: Literal["weekly", "monthly", "quarterly", "yearly"]
    dry_run: bool
    raw_articles: str
    previous_entries: str
    gaps: str
    research: str
    synthesis: str
    written: bool


def log_step(step: str, status: str) -> None:
    print(f"ai-landscape-review: {step} {status}", file=sys.stderr)


def _run_command(command: list[str]) -> str:
    result = subprocess.run(command, capture_output=True, text=True, timeout=120, check=False)
    if result.returncode != 0:
        raise RuntimeError(f"command failed ({' '.join(command)}): {result.stderr.strip()}")
    return result.stdout.strip()


def _period_start(period: Period, today: date | None = None) -> date:
    current = today or date.today()
    days = {
        "weekly": 7,
        "monthly": 30,
        "quarterly": 90,
        "yearly": 365,
    }[period]
    return current - timedelta(days=days)


def _read_text(path: Path) -> str:
    if not path.exists():
        raise FileNotFoundError(f"missing required file: {path}")
    return path.read_text(encoding="utf-8")


def _extract_recent_news_entries(content: str, since: date) -> str:
    pattern = re.compile(
        r"^## (?P<date>\d{4}-\d{2}-\d{2}) \([^)]+\)\n(?P<body>.*?)(?=^## \d{4}-\d{2}-\d{2} \([^)]+\)\n|\Z)",
        re.MULTILINE | re.DOTALL,
    )
    matches: list[str] = []
    for match in pattern.finditer(content):
        entry_date = datetime.strptime(match.group("date"), "%Y-%m-%d").date()
        if entry_date >= since:
            matches.append(match.group(0).strip())
    return "\n\n".join(matches)


def _extract_previous_entries(content: str, period: Period) -> str:
    if period == "weekly":
        weekly_index = content.find(WEEKLY_MARKER)
        if weekly_index == -1:
            return ""
        return content[weekly_index : weekly_index + 6000].strip()
    if period == "monthly":
        start = content.find(MONTHLY_HEADER)
        if start == -1:
            return ""
        return content[start : start + 5000].strip()
    if period == "quarterly":
        start = content.find(QUARTERLY_HEADER)
        if start != -1:
            return content[start : start + 5000].strip()
        fallback = content.find(MONTHLY_HEADER)
        return content[fallback : fallback + 3000].strip() if fallback != -1 else ""
    start = content.find(YEARLY_HEADER)
    if start != -1:
        return content[start : start + 5000].strip()
    fallback = content.find(QUARTERLY_HEADER)
    return content[fallback : fallback + 3000].strip() if fallback != -1 else ""


def gather_node(state: PipelineState) -> PipelineState:
    log_step("gather", "starting")
    period = state["period"]
    since = _period_start(period)
    lustro_output = _run_command(["lustro", "log", "-n", "200"])
    ai_landscape = _read_text(AI_LANDSCAPE_PATH)
    ai_news_log = _read_text(AI_NEWS_LOG_PATH)
    recent_news = _extract_recent_news_entries(ai_news_log, since)
    combined = "\n\n".join(
        section for section in [
            "# Lustro Log",
            lustro_output,
            "# AI News Log Entries",
            recent_news,
        ] if section
    )
    log_step("gather", "complete")
    return {
        **state,
        "raw_articles": combined,
        "previous_entries": _extract_previous_entries(ai_landscape, period),
    }


def gap_analysis_node(state: PipelineState) -> PipelineState:
    log_step("gap_analysis", "starting")
    prompt = build_gap_analysis_prompt(state["period"], state["raw_articles"])
    gaps = call_opus(prompt)
    log_step("gap_analysis", "complete")
    return {**state, "gaps": gaps}


def should_research(state: PipelineState) -> str:
    return "skip_research" if state["gaps"].strip() == "NO GAPS" else "research"


def research_node(state: PipelineState) -> PipelineState:
    log_step("research", "starting")
    research = call_opus(build_research_prompt(state["gaps"]))
    log_step("research", "complete")
    return {**state, "research": research}


def synthesise_node(state: PipelineState) -> PipelineState:
    log_step("synthesise", "starting")
    prompt = build_synthesis_prompt(
        state["period"],
        state["raw_articles"],
        state["previous_entries"],
        state["research"],
    )
    synthesis = call_opus(prompt)
    log_step("synthesise", "complete")
    return {**state, "synthesis": synthesis}


def _find_or_create_section(content: str, header: str, insert_before: str | None = None) -> str:
    if header in content:
        return content
    insertion = f"\n\n{header}\n\n<!-- {header[3:]} below, most recent first -->\n"
    if insert_before and insert_before in content:
        return content.replace(insert_before, insertion + "\n" + insert_before, 1)
    return content.rstrip() + insertion + "\n"


def _extract_title(synthesis: str) -> str:
    match = re.search(r"^\*\*Title:\*\*\s*(.+)$", synthesis, re.MULTILINE)
    if match:
        return match.group(1).strip()
    for line in synthesis.splitlines():
        cleaned = line.strip().strip("#").strip("*").strip()
        if cleaned:
            return cleaned[:80]
    raise ValueError("unable to extract title from synthesis")


def _period_heading(period: Period, synthesis: str, today: date | None = None) -> str:
    current = today or date.today()
    if period == "weekly":
        return f"### Week of {current.isoformat()}"
    if period == "monthly":
        return f"### {current.strftime('%Y-%m')} — {_extract_title(synthesis)}"
    if period == "quarterly":
        quarter = ((current.month - 1) // 3) + 1
        return f"### {current.year}-Q{quarter} — {_extract_title(synthesis)}"
    return f"### {current.year} — {_extract_title(synthesis)}"


def _insert_after_marker(content: str, marker: str, block: str) -> str:
    if marker not in content:
        raise ValueError(f"marker not found: {marker}")
    return content.replace(marker, f"{marker}\n\n{block}", 1)


def _insert_after_header(content: str, header: str, block: str) -> str:
    if header not in content:
        raise ValueError(f"header not found: {header}")
    pattern = re.compile(rf"({re.escape(header)}\n(?:<!--.*?-->\n)?)", re.DOTALL)
    match = pattern.search(content)
    if not match:
        raise ValueError(f"unable to place block under header: {header}")
    return content[: match.end()] + "\n" + block + content[match.end() :]


def _update_current_landscape(content: str, synthesis: str) -> str:
    pattern = re.compile(
        rf"({re.escape(CURRENT_LANDSCAPE_HEADER)}\n)(.*?)(\n---)",
        re.DOTALL,
    )
    replacement = f"{CURRENT_LANDSCAPE_HEADER}\n\n{synthesis.strip()}\n\n---"
    updated, count = pattern.subn(replacement, content, count=1)
    if count == 0:
        raise ValueError("unable to update Current Landscape section")
    return updated


def _write_landscape(content: str, period: Period, synthesis: str) -> str:
    heading = _period_heading(period, synthesis)
    block = f"{heading}\n\n{synthesis.strip()}\n"
    updated = content
    if period == "weekly":
        return _insert_after_marker(updated, WEEKLY_MARKER, block)
    if period == "monthly":
        updated = _update_current_landscape(updated, synthesis)
        updated = _insert_after_header(updated, MONTHLY_HEADER, block)
        return updated
    if period == "quarterly":
        updated = _find_or_create_section(updated, QUARTERLY_HEADER, MONTHLY_HEADER)
        return _insert_after_header(updated, QUARTERLY_HEADER, block)
    updated = _find_or_create_section(updated, YEARLY_HEADER, QUARTERLY_HEADER if QUARTERLY_HEADER in updated else MONTHLY_HEADER)
    return _insert_after_header(updated, YEARLY_HEADER, block)


def write_node(state: PipelineState) -> PipelineState:
    log_step("write", "starting")
    if state["dry_run"]:
        log_step("write", "complete")
        return {**state, "written": False}

    original = _read_text(AI_LANDSCAPE_PATH)
    updated = _write_landscape(original, state["period"], state["synthesis"])
    AI_LANDSCAPE_PATH.write_text(updated, encoding="utf-8")
    commit_date = date.today().isoformat()
    _run_command(["git", "-C", str(NOTES_DIR), "add", "AI Landscape.md"])
    _run_command(
        [
            "git",
            "-C",
            str(NOTES_DIR),
            "commit",
            "-m",
            f"ai-landscape: {state['period']} snapshot {commit_date}",
        ]
    )
    log_step("write", "complete")
    return {**state, "written": True}
