"""Self-model — maintains a living picture of what Alive can and can't do."""

from __future__ import annotations

import json
import re
import sqlite3
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from alive.core.skill_registry import SkillInfo, SkillRegistry, get_registry
from alive.llm.base import LLMProvider


# ── Data structures ─────────────────────────────────────────────────────────


@dataclass
class GapRecord:
    """A capability that was requested but failed to build."""

    id: int
    request: str
    error: str
    timestamp: str
    resolved: bool = False
    resolved_by: str = ""


@dataclass
class RequestRecord:
    """A logged user request and its outcome."""

    id: int
    request_text: str
    timestamp: str
    status: str  # 'success', 'failed', 'partial'
    skills_used: list[str] = field(default_factory=list)
    skills_built: list[str] = field(default_factory=list)


@dataclass
class SelfModel:
    """Snapshot of Alive's current capabilities and gaps."""

    total_skills: int
    total_executions: int
    overall_success_rate: float
    high_confidence: list[SkillInfo] = field(default_factory=list)
    medium_confidence: list[SkillInfo] = field(default_factory=list)
    low_confidence: list[SkillInfo] = field(default_factory=list)
    domains: dict[str, list[SkillInfo]] = field(default_factory=dict)
    gaps: list[GapRecord] = field(default_factory=list)
    growth_timeline: dict[str, int] = field(default_factory=dict)


# ── Gap tracking (SQLite) ──────────────────────────────────────────────────

_CREATE_GAPS_TABLE = """\
CREATE TABLE IF NOT EXISTS gaps (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    request TEXT NOT NULL,
    error TEXT NOT NULL,
    timestamp TEXT NOT NULL,
    resolved INTEGER DEFAULT 0,
    resolved_by TEXT DEFAULT ''
)"""

_CREATE_REQUESTS_TABLE = """\
CREATE TABLE IF NOT EXISTS requests (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    request_text TEXT NOT NULL,
    timestamp TEXT NOT NULL,
    status TEXT NOT NULL,
    skills_used TEXT DEFAULT '[]',
    skills_built TEXT DEFAULT '[]'
)"""


def _ensure_db(db_path: Path) -> sqlite3.Connection:
    """Open (or create) the database and ensure all tables exist."""
    db_path.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(db_path))
    conn.execute(_CREATE_GAPS_TABLE)
    conn.execute(_CREATE_REQUESTS_TABLE)
    conn.commit()
    return conn


def record_gap(
    request: str,
    error: str,
    *,
    db_path: Path | None = None,
) -> GapRecord:
    """Record a capability gap (failed skill build)."""
    from alive.config import DB_PATH

    db_path = db_path or DB_PATH
    now = datetime.now(timezone.utc).isoformat()
    conn = _ensure_db(db_path)
    try:
        cur = conn.execute(
            "INSERT INTO gaps (request, error, timestamp) VALUES (?, ?, ?)",
            (request, error[:2000], now),
        )
        conn.commit()
        return GapRecord(
            id=cur.lastrowid or 0,
            request=request,
            error=error[:2000],
            timestamp=now,
        )
    finally:
        conn.close()


def resolve_gap(
    gap_id: int,
    skill_name: str,
    *,
    db_path: Path | None = None,
) -> None:
    """Mark a gap as resolved by a newly built skill."""
    from alive.config import DB_PATH

    db_path = db_path or DB_PATH
    conn = _ensure_db(db_path)
    try:
        conn.execute(
            "UPDATE gaps SET resolved = 1, resolved_by = ? WHERE id = ?",
            (skill_name, gap_id),
        )
        conn.commit()
    finally:
        conn.close()


def list_gaps(
    *,
    include_resolved: bool = False,
    db_path: Path | None = None,
) -> list[GapRecord]:
    """Fetch gap records from the database."""
    from alive.config import DB_PATH

    db_path = db_path or DB_PATH
    if not db_path.exists():
        return []
    conn = _ensure_db(db_path)
    try:
        if include_resolved:
            rows = conn.execute(
                "SELECT id, request, error, timestamp, resolved, resolved_by "
                "FROM gaps ORDER BY id DESC"
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT id, request, error, timestamp, resolved, resolved_by "
                "FROM gaps WHERE resolved = 0 ORDER BY id DESC"
            ).fetchall()
        return [
            GapRecord(
                id=r[0],
                request=r[1],
                error=r[2],
                timestamp=r[3],
                resolved=bool(r[4]),
                resolved_by=r[5],
            )
            for r in rows
        ]
    finally:
        conn.close()


# ── Request logging (SQLite) ────────────────────────────────────────────────


def record_request(
    request_text: str,
    status: str,
    skills_used: list[str] | None = None,
    skills_built: list[str] | None = None,
    *,
    db_path: Path | None = None,
) -> RequestRecord:
    """Log a user request and its outcome."""
    from alive.config import DB_PATH

    db_path = db_path or DB_PATH
    now = datetime.now(timezone.utc).isoformat()
    used = json.dumps(skills_used or [])
    built = json.dumps(skills_built or [])
    conn = _ensure_db(db_path)
    try:
        cur = conn.execute(
            "INSERT INTO requests (request_text, timestamp, status, skills_used, skills_built) "
            "VALUES (?, ?, ?, ?, ?)",
            (request_text, now, status, used, built),
        )
        conn.commit()
        return RequestRecord(
            id=cur.lastrowid or 0,
            request_text=request_text,
            timestamp=now,
            status=status,
            skills_used=skills_used or [],
            skills_built=skills_built or [],
        )
    finally:
        conn.close()


def list_requests(
    *,
    days: int = 7,
    db_path: Path | None = None,
) -> list[RequestRecord]:
    """Fetch recent request records from the database."""
    from alive.config import DB_PATH

    db_path = db_path or DB_PATH
    if not db_path.exists():
        return []
    conn = _ensure_db(db_path)
    try:
        rows = conn.execute(
            "SELECT id, request_text, timestamp, status, skills_used, skills_built "
            "FROM requests WHERE timestamp >= datetime('now', ?) ORDER BY id DESC",
            (f"-{days} days",),
        ).fetchall()
        return [
            RequestRecord(
                id=r[0],
                request_text=r[1],
                timestamp=r[2],
                status=r[3],
                skills_used=json.loads(r[4]) if r[4] else [],
                skills_built=json.loads(r[5]) if r[5] else [],
            )
            for r in rows
        ]
    finally:
        conn.close()


# ── Domain classification ──────────────────────────────────────────────────

VALID_DOMAINS = ["web", "file", "data", "system", "text", "math", "other"]

_DOMAIN_KEYWORDS: dict[str, list[str]] = {
    "web": ["http", "url", "fetch", "web", "api", "request", "download", "scrape", "html", "json"],
    "file": ["file", "read", "write", "path", "directory", "csv", "parse", "log"],
    "data": ["database", "sql", "query", "transform", "filter", "aggregate", "pandas", "dataframe"],
    "system": ["shell", "command", "process", "run", "exec", "os", "env", "install", "package"],
    "text": ["text", "string", "regex", "format", "template", "markdown", "summarize", "translate"],
    "math": ["math", "calculate", "fibonacci", "prime", "number", "random", "statistics", "convert"],
}


def classify_skill_keyword(skill: SkillInfo) -> str:
    """Classify a skill into a domain using keyword matching."""
    corpus = f"{skill.name} {skill.description}".lower()
    best_domain = "other"
    best_score = 0
    for domain, keywords in _DOMAIN_KEYWORDS.items():
        score = sum(1 for kw in keywords if kw in corpus)
        if score > best_score:
            best_score = score
            best_domain = domain
    return best_domain


_CLASSIFY_SYSTEM = """\
You are classifying AI agent skills into domains. Given a list of skills \
(name and description), classify each into exactly one domain.

Valid domains: web, file, data, system, text, math, other

Respond with ONLY a JSON object mapping skill names to domains:
{"skill_name": "domain", ...}"""


async def classify_skills_llm(
    skills: list[SkillInfo],
    provider: LLMProvider,
) -> dict[str, str]:
    """Classify skills into domains via a single LLM call."""
    if not skills:
        return {}
    skill_lines = "\n".join(
        f"- {s.name}: {s.description}" for s in skills
    )
    user_prompt = f"Classify these skills:\n{skill_lines}"
    try:
        response = await provider.complete(_CLASSIFY_SYSTEM, user_prompt)
        # Parse JSON from response
        stripped = response.strip()
        try:
            data = json.loads(stripped)
        except json.JSONDecodeError:
            match = re.search(r"\{.*\}", stripped, re.DOTALL)
            if match:
                data = json.loads(match.group())
            else:
                return {}
        if not isinstance(data, dict):
            return {}
        # Validate domains
        return {
            name: domain
            for name, domain in data.items()
            if isinstance(domain, str) and domain in VALID_DOMAINS
        }
    except Exception as exc:
        import logging
        logging.getLogger("alive").debug("LLM classification failed: %s", exc)
        return {}


def _classify_and_persist(
    skills: list[SkillInfo],
    registry: SkillRegistry,
    llm_results: dict[str, str] | None = None,
) -> None:
    """Classify unclassified skills and save domain to metadata."""
    for skill in skills:
        if skill.domain:
            continue
        if llm_results and skill.name in llm_results:
            skill.domain = llm_results[skill.name]
        else:
            skill.domain = classify_skill_keyword(skill)
        registry._save_metadata(skill)


# ── Build self-model ───────────────────────────────────────────────────────


def _build_growth_timeline(skills: list[SkillInfo]) -> dict[str, int]:
    """Group skills by ISO week of creation."""
    timeline: dict[str, int] = {}
    for skill in skills:
        if not skill.created_at:
            continue
        try:
            dt = datetime.fromisoformat(skill.created_at)
            year, week, _ = dt.isocalendar()
            key = f"{year}-W{week:02d}"
            timeline[key] = timeline.get(key, 0) + 1
        except (ValueError, TypeError):
            continue
    return dict(sorted(timeline.items()))


async def build_self_model(
    *,
    registry: SkillRegistry | None = None,
    provider: LLMProvider | None = None,
    db_path: Path | None = None,
) -> SelfModel:
    """Build a complete self-model snapshot.

    Args:
        registry: Skill registry to read from. Defaults to global registry.
        provider: If given, use LLM to classify unclassified skills.
        db_path: SQLite database path. Defaults to ~/.alive/alive.db.
    """
    registry = registry or get_registry()
    skills = registry.list_all()

    # Domain classification
    unclassified = [s for s in skills if not s.domain]
    llm_results: dict[str, str] | None = None
    if provider and unclassified:
        llm_results = await classify_skills_llm(unclassified, provider)
    _classify_and_persist(skills, registry, llm_results)

    # Group by domain
    domains: dict[str, list[SkillInfo]] = defaultdict(list)
    for skill in skills:
        domains[skill.domain or "other"].append(skill)

    # Confidence buckets
    high = [s for s in skills if s.confidence_score >= 0.8]
    medium = [s for s in skills if 0.5 <= s.confidence_score < 0.8]
    low = [s for s in skills if s.confidence_score < 0.5]

    # Aggregate stats
    total_executions = sum(s.times_used for s in skills)
    total_successes = sum(s.success_count for s in skills)
    success_rate = (
        total_successes / total_executions if total_executions > 0 else 0.0
    )

    # Gaps
    gaps = list_gaps(db_path=db_path)

    # Growth timeline
    growth = _build_growth_timeline(skills)

    return SelfModel(
        total_skills=len(skills),
        total_executions=total_executions,
        overall_success_rate=success_rate,
        high_confidence=high,
        medium_confidence=medium,
        low_confidence=low,
        domains=dict(domains),
        gaps=gaps,
        growth_timeline=growth,
    )


# ── Rich display ───────────────────────────────────────────────────────────


def _bar(count: int, max_count: int, width: int = 20) -> str:
    """Build a text bar: ████░░░░."""
    if max_count == 0:
        return "░" * width
    filled = round(count / max_count * width)
    return "█" * filled + "░" * (width - filled)


def _relative_time(iso_timestamp: str) -> str:
    """Convert ISO timestamp to human-readable relative time."""
    try:
        dt = datetime.fromisoformat(iso_timestamp)
        now = datetime.now(timezone.utc)
        delta = now - dt
        days = delta.days
        if days == 0:
            hours = delta.seconds // 3600
            if hours == 0:
                return "just now"
            return f"{hours}h ago"
        if days == 1:
            return "yesterday"
        if days < 7:
            return f"{days}d ago"
        weeks = days // 7
        if weeks == 1:
            return "1 week ago"
        return f"{weeks} weeks ago"
    except (ValueError, TypeError):
        return "unknown"


def display_self_model(model: SelfModel, console: Console | None = None) -> None:
    """Render the self-model as rich terminal output."""
    console = console or Console()

    # ── Section 1: Capability Overview ──
    lines = Text()
    lines.append("Skills: ", style="bold")
    lines.append(f"{model.total_skills}    ")
    lines.append("Executions: ", style="bold")
    lines.append(f"{model.total_executions}    ")
    lines.append("Success: ", style="bold")
    lines.append(f"{model.overall_success_rate:.0%}\n")

    if model.domains:
        lines.append("\nCapabilities by domain:\n", style="bold")
        max_count = max(len(skills) for skills in model.domains.values())
        for domain in sorted(model.domains, key=lambda d: len(model.domains[d]), reverse=True):
            skills = model.domains[domain]
            n = len(skills)
            bar = _bar(n, max_count)
            label = f"  {domain:<8} {bar}  {n} skill{'s' if n != 1 else ''}\n"
            lines.append(label)

    lines.append("\nConfidence:\n", style="bold")
    if model.high_confidence:
        names = ", ".join(s.name for s in model.high_confidence)
        lines.append(f"  ", style="green")
        lines.append("● ", style="green")
        lines.append(f"High (≥80%): {names}\n")
    if model.medium_confidence:
        names = ", ".join(s.name for s in model.medium_confidence)
        lines.append(f"  ", style="yellow")
        lines.append("● ", style="yellow")
        lines.append(f"Medium: {names}\n")
    if model.low_confidence:
        names = ", ".join(s.name for s in model.low_confidence)
        lines.append(f"  ", style="red")
        lines.append("● ", style="red")
        lines.append(f"Low (<50%): {names}\n")
    if not model.high_confidence and not model.medium_confidence and not model.low_confidence:
        lines.append("  [dim]No skills yet[/]\n")

    console.print(
        Panel(lines, title="[bold green]Alive Self-Model[/]", border_style="green")
    )

    # ── Section 2: Known Gaps ──
    if model.gaps:
        table = Table(title="Known Gaps", border_style="red")
        table.add_column("#", style="dim", justify="right")
        table.add_column("Request")
        table.add_column("When", justify="right")
        table.add_column("Status", justify="center")
        for gap in model.gaps[:10]:
            status = (
                f"[green]resolved ({gap.resolved_by})[/]"
                if gap.resolved
                else "[red]open[/]"
            )
            table.add_row(
                str(gap.id),
                gap.request[:60],
                _relative_time(gap.timestamp),
                status,
            )
        console.print(table)
    else:
        console.print(
            Panel(
                "[dim]No known gaps — all requested capabilities were built successfully.[/]",
                title="Known Gaps",
                border_style="dim",
            )
        )

    # ── Section 3: Growth Timeline ──
    if model.growth_timeline:
        growth_lines = Text()
        max_week = max(model.growth_timeline.values())
        for week, count in model.growth_timeline.items():
            bar = _bar(count, max_week, width=16)
            growth_lines.append(
                f"  {week}  {bar}  {count} skill{'s' if count != 1 else ''}\n"
            )
        console.print(
            Panel(
                growth_lines,
                title="[bold cyan]Growth[/]",
                border_style="cyan",
            )
        )
    else:
        console.print(
            Panel(
                "[dim]No growth data yet.[/]",
                title="Growth",
                border_style="dim",
            )
        )
