"""Graph — visual map of skill relationships and clusters."""

from __future__ import annotations

import json
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from itertools import combinations
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree

from alive.core.chain import ChainInfo, list_chains
from alive.core.self_model import (
    RequestRecord,
    classify_skill_keyword,
    classify_skills_llm,
    list_requests,
    _classify_and_persist,
)
from alive.core.skill_registry import SkillInfo, SkillRegistry, get_registry
from alive.llm.base import LLMProvider

_log = logging.getLogger("alive")


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


@dataclass
class SkillEdge:
    """A relationship between two skills."""

    source: str
    target: str
    edge_type: str  # "chain", "co_usage", "co_creation"
    weight: int = 1


@dataclass
class SkillCluster:
    """A group of related skills."""

    name: str
    emoji: str
    skills: list[str] = field(default_factory=list)


@dataclass
class SkillGraph:
    """Complete skill relationship graph."""

    skills: list[SkillInfo] = field(default_factory=list)
    edges: list[SkillEdge] = field(default_factory=list)
    clusters: list[SkillCluster] = field(default_factory=list)
    chains: list[ChainInfo] = field(default_factory=list)


# ── Domain emoji mapping ───────────────────────────────────────────────────

_DOMAIN_EMOJI: dict[str, str] = {
    "web": "🌐",
    "file": "📁",
    "data": "📊",
    "system": "🔧",
    "text": "📝",
    "math": "🔢",
    "other": "⚡",
}


# ── Edge builders ──────────────────────────────────────────────────────────


def _build_chain_edges(chains: list[ChainInfo]) -> list[SkillEdge]:
    """Build directed edges from chain step sequences."""
    edges: list[SkillEdge] = []
    for chain in chains:
        for i in range(len(chain.steps) - 1):
            edges.append(SkillEdge(
                source=chain.steps[i],
                target=chain.steps[i + 1],
                edge_type="chain",
            ))
    return edges


def _build_co_usage_edges(requests: list[RequestRecord]) -> list[SkillEdge]:
    """Build edges between skills used together in the same request."""
    pair_counts: dict[tuple[str, str], int] = defaultdict(int)

    for req in requests:
        skills = sorted(set(req.skills_used))
        if len(skills) < 2:
            continue
        for a, b in combinations(skills, 2):
            pair_counts[(a, b)] += 1

    return [
        SkillEdge(source=a, target=b, edge_type="co_usage", weight=count)
        for (a, b), count in sorted(pair_counts.items())
    ]


def _build_co_creation_edges(
    skills_dir: Path | None = None,
) -> list[SkillEdge]:
    """Build edges between skills that were created in the same request."""
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    if not skills_dir.exists():
        return []

    # Group skills by their original request
    request_groups: dict[str, list[str]] = defaultdict(list)

    for skill_dir in skills_dir.iterdir():
        if not skill_dir.is_dir():
            continue
        story_file = skill_dir / "birth_story.json"
        if not story_file.exists():
            continue
        try:
            data = json.loads(story_file.read_text())
            original_request = data.get("original_request", "")
            if original_request:
                request_groups[original_request].append(skill_dir.name)
        except (json.JSONDecodeError, OSError):
            continue

    # Create edges between skills that share an original request
    edges: list[SkillEdge] = []
    for _request, skill_names in request_groups.items():
        if len(skill_names) < 2:
            continue
        for a, b in combinations(sorted(skill_names), 2):
            edges.append(SkillEdge(
                source=a, target=b, edge_type="co_creation",
            ))

    return edges


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


async def _classify_all(
    skills: list[SkillInfo],
    registry: SkillRegistry,
    *,
    provider: LLMProvider | None = None,
) -> None:
    """Classify unclassified skills into domains and persist.

    Uses keyword matching by default. If a provider is given,
    uses LLM classification for unclassified skills.
    """
    unclassified = [s for s in skills if not s.domain]
    if not unclassified:
        return

    llm_results: dict[str, str] | None = None
    if provider is not None:
        try:
            llm_results = await classify_skills_llm(unclassified, provider)
        except Exception as exc:
            _log.debug("LLM classification failed: %s", exc)

    _classify_and_persist(unclassified, registry, llm_results)


# ── Cluster builder ────────────────────────────────────────────────────────


def _build_clusters(
    skills: list[SkillInfo],
    edges: list[SkillEdge],
) -> list[SkillCluster]:
    """Group skills into clusters by domain.

    Skills with domain="other" and no edges go to "Standalone".
    """
    # Track which skills have any edges
    connected: set[str] = set()
    for edge in edges:
        connected.add(edge.source)
        connected.add(edge.target)

    # Group by domain
    domain_groups: dict[str, list[str]] = defaultdict(list)
    standalone: list[str] = []

    for skill in skills:
        domain = skill.domain or "other"
        if domain == "other" and skill.name not in connected:
            standalone.append(skill.name)
        else:
            domain_groups[domain].append(skill.name)

    # Build clusters
    clusters: list[SkillCluster] = []

    # Known domains first (sorted alphabetically)
    for domain in sorted(domain_groups):
        emoji = _DOMAIN_EMOJI.get(domain, "⚡")
        label = domain.capitalize()
        clusters.append(SkillCluster(
            name=f"{label} cluster",
            emoji=emoji,
            skills=sorted(domain_groups[domain]),
        ))

    # Standalone at the end
    if standalone:
        clusters.append(SkillCluster(
            name="Standalone",
            emoji="🔧",
            skills=sorted(standalone),
        ))

    return clusters


# ── Core graph builder ──────────────────────────────────────────────────────


async def build_graph(
    *,
    registry: SkillRegistry | None = None,
    provider: LLMProvider | None = None,
    db_path: Path | None = None,
    skills_dir: Path | None = None,
) -> SkillGraph:
    """Build a complete skill relationship graph.

    Combines data from chains, request history, birth stories, and
    skill metadata to produce a graph with edges and clusters.

    Args:
        registry: SkillRegistry override. Defaults to global registry.
        provider: LLM provider for domain classification. Optional.
        db_path: Database path override.
        skills_dir: Skills directory override.

    Returns:
        SkillGraph with skills, edges, clusters, and chains.
    """
    registry = registry or get_registry()

    # Get all regular skills (exclude chains)
    skills = registry.list_skills_only()

    # Get all chains
    chains = list_chains(skills_dir=skills_dir)

    # Classify domains
    await _classify_all(skills, registry, provider=provider)

    # Build edges from all sources
    chain_edges = _build_chain_edges(chains)

    try:
        requests = list_requests(days=365, db_path=db_path)
    except Exception as exc:
        _log.debug("Failed to load requests: %s", exc)
        requests = []
    co_usage_edges = _build_co_usage_edges(requests)

    co_creation_edges = _build_co_creation_edges(skills_dir=skills_dir)

    all_edges = chain_edges + co_usage_edges + co_creation_edges

    # Build clusters
    clusters = _build_clusters(skills, all_edges)

    return SkillGraph(
        skills=skills,
        edges=all_edges,
        clusters=clusters,
        chains=chains,
    )


# ── Display ─────────────────────────────────────────────────────────────────


def display_graph(
    graph: SkillGraph,
    *,
    console: Console | None = None,
) -> None:
    """Render the skill graph as a rich panel with nested trees."""
    con = console or Console()

    if not graph.skills and not graph.chains:
        con.print(Panel(
            "[dim]No skills installed yet.[/]\n\n"
            "Build one with: [cyan]alive build \"description\"[/]",
            title="[bold]Skill Map[/]",
            border_style="yellow",
        ))
        return

    n_skills = len(graph.skills)
    n_clusters = len(graph.clusters)
    title = f"Skill Map ({n_skills} skills, {n_clusters} clusters)"

    tree = Tree(f"[bold]{title}[/]")

    # Build a quick lookup: source → list of chain targets
    chain_targets: dict[str, list[str]] = defaultdict(list)
    for edge in graph.edges:
        if edge.edge_type == "chain":
            chain_targets[edge.source].append(edge.target)

    # Render clusters
    for cluster in graph.clusters:
        branch = tree.add(f"{cluster.emoji} [bold]{cluster.name}[/]")
        for skill_name in cluster.skills:
            skill_node = branch.add(f"[green]{skill_name}[/]")
            # Show chain connections from this skill
            for target in chain_targets.get(skill_name, []):
                skill_node.add(f"[dim]──→[/] {target}")

    # Render chains section
    if graph.chains:
        chain_branch = tree.add("🔗 [bold]Chains[/]")
        for chain in graph.chains:
            steps_text = " → ".join(chain.steps)
            chain_branch.add(
                f"[cyan]{chain.name}[/] ({steps_text})"
            )

    con.print()
    con.print(Panel(tree, border_style="cyan"))


# ── DOT export ──────────────────────────────────────────────────────────────

_DOT_DOMAIN_COLORS: dict[str, str] = {
    "web": "#4A90D9",
    "file": "#F5A623",
    "data": "#7B68EE",
    "system": "#50C878",
    "text": "#FF6B6B",
    "math": "#FFD700",
    "other": "#C0C0C0",
}

_DOT_EDGE_STYLES: dict[str, str] = {
    "chain": "solid",
    "co_usage": "dashed",
    "co_creation": "dotted",
}


def export_dot(graph: SkillGraph) -> str:
    """Export the skill graph as a Graphviz DOT format string.

    Nodes are colored by domain. Edges are styled by type:
    - chain: solid arrows
    - co_usage: dashed lines
    - co_creation: dotted lines
    """
    lines: list[str] = [
        "digraph skills {",
        '    rankdir=LR;',
        '    node [shape=box, style="rounded,filled", fontname="Helvetica"];',
        '    edge [fontname="Helvetica", fontsize=10];',
        "",
    ]

    # Build skill→domain lookup
    skill_domains: dict[str, str] = {}
    for skill in graph.skills:
        skill_domains[skill.name] = skill.domain or "other"

    # Emit nodes grouped by cluster
    for cluster in graph.clusters:
        domain_key = cluster.name.replace(" cluster", "").lower()
        color = _DOT_DOMAIN_COLORS.get(domain_key, "#C0C0C0")
        lines.append(f"    // {cluster.emoji} {cluster.name}")
        for skill_name in cluster.skills:
            safe_name = skill_name.replace("-", "_")
            lines.append(
                f'    {safe_name} [label="{skill_name}", fillcolor="{color}"];'
            )
        lines.append("")

    # Emit edges
    if graph.edges:
        lines.append("    // Edges")
        for edge in graph.edges:
            src = edge.source.replace("-", "_")
            tgt = edge.target.replace("-", "_")
            style = _DOT_EDGE_STYLES.get(edge.edge_type, "solid")
            label = ""
            if edge.edge_type == "co_usage" and edge.weight > 1:
                label = f', label="{edge.weight}x"'
            lines.append(
                f'    {src} -> {tgt} [style={style}{label}];'
            )
        lines.append("")

    # Emit chain subgraphs
    for chain in graph.chains:
        safe_name = chain.name.replace("-", "_")
        lines.append(f"    // Chain: {chain.name}")
        lines.append(f"    subgraph cluster_{safe_name} {{")
        lines.append(f'        label="{chain.name}";')
        lines.append('        style=dashed;')
        lines.append('        color="#888888";')
        step_names = " -> ".join(s.replace("-", "_") for s in chain.steps)
        lines.append(f"        {step_names};")
        lines.append("    }")
        lines.append("")

    lines.append("}")
    return "\n".join(lines)
