"""Skill chaining — compose multiple skills into named pipelines."""

from __future__ import annotations

import json
import shutil
import subprocess
import time
from dataclasses import asdict, 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


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


@dataclass
class ChainInfo:
    """Metadata for a skill chain."""

    name: str
    steps: list[str]
    created_at: str = ""
    default_input: dict[str, Any] = field(default_factory=dict)
    schedule: str | None = None
    description: str = ""
    times_run: int = 0
    last_run: str = ""


@dataclass
class ChainStepResult:
    """Result of one step within a chain execution."""

    skill_name: str
    success: bool
    output: dict[str, Any] | None = None
    error: str | None = None
    duration_ms: int = 0


@dataclass
class ChainResult:
    """Result of a full chain execution."""

    chain_name: str
    step_results: list[ChainStepResult]
    final_output: dict[str, Any] | None = None
    success: bool = False
    total_duration_ms: int = 0


# ── CRUD functions ──────────────────────────────────────────────────────────


def create_chain(
    name: str,
    steps: list[str],
    *,
    default_input: dict[str, Any] | None = None,
    schedule: str | None = None,
    skills_dir: Path | None = None,
) -> ChainInfo:
    """Create a new skill chain.

    Args:
        name: Chain name (used as directory name and command).
        steps: Ordered list of skill names to chain.
        default_input: Default kwargs for the first skill. If None, uses
            the first skill's sample_input from the registry.
        schedule: Optional cron expression for scheduling.
        skills_dir: Override skills directory.

    Returns:
        The created ChainInfo.

    Raises:
        ValueError: If fewer than 2 steps or a referenced skill doesn't exist.
        FileExistsError: If a chain/skill with this name already exists.
    """
    from alive.config import SKILLS_DIR
    from alive.core.skill_registry import get_registry

    skills_dir = skills_dir or SKILLS_DIR
    registry = get_registry()

    # Validate
    if len(steps) < 2:
        raise ValueError("A chain requires at least 2 skills.")

    for step_name in steps:
        try:
            registry.get_skill(step_name)
        except KeyError:
            raise ValueError(
                f"Skill '{step_name}' not found. "
                f"Build it first with: alive build \"{step_name}\""
            )

    chain_dir = skills_dir / name
    if chain_dir.exists():
        raise FileExistsError(
            f"A skill or chain named '{name}' already exists.\n"
            f"Delete it first with: alive unchain {name}"
        )

    # Resolve default input
    if default_input is None:
        first_skill = registry.get_skill(steps[0])
        default_input = first_skill.sample_input or {}

    # Build description
    description = f"Chain: {' → '.join(steps)}"

    now = datetime.now(timezone.utc).isoformat()
    chain_info = ChainInfo(
        name=name,
        steps=steps,
        created_at=now,
        default_input=default_input,
        schedule=schedule,
        description=description,
    )

    # Write to disk
    chain_dir.mkdir(parents=True, exist_ok=True)

    # chain.json — full chain metadata
    chain_path = chain_dir / "chain.json"
    chain_path.write_text(json.dumps(asdict(chain_info), indent=2) + "\n")

    # metadata.json — registry-compatible metadata
    meta = {
        "name": name,
        "description": description,
        "created_at": now,
        "times_used": 0,
        "success_count": 0,
        "fail_count": 0,
        "last_used": "",
        "dependencies": [],
        "confidence_score": 0.5,
        "input_schema": {},
        "output_schema": {},
        "sample_input": default_input,
        "domain": "",
        "skill_type": "chain",
    }
    (chain_dir / "metadata.json").write_text(json.dumps(meta, indent=2) + "\n")

    # Reload registry to pick up the new chain
    registry._skills[name] = __import__(
        "alive.core.skill_registry", fromlist=["SkillInfo"]
    ).SkillInfo(**{k: v for k, v in meta.items() if k in _skillinfo_fields()})

    return chain_info


def _skillinfo_fields() -> set[str]:
    """Return the set of field names that SkillInfo accepts."""
    from alive.core.skill_registry import SkillInfo

    return set(SkillInfo.__dataclass_fields__.keys())


def load_chain(name: str, *, skills_dir: Path | None = None) -> ChainInfo:
    """Load a chain from disk.

    Raises:
        FileNotFoundError: If the chain doesn't exist.
    """
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    chain_path = skills_dir / name / "chain.json"

    if not chain_path.exists():
        raise FileNotFoundError(
            f"No chain named '{name}' found.\n\n"
            f"Create one with: alive chain {name} skill1 skill2 ..."
        )

    data = json.loads(chain_path.read_text())
    return ChainInfo(
        name=data["name"],
        steps=data["steps"],
        created_at=data.get("created_at", ""),
        default_input=data.get("default_input", {}),
        schedule=data.get("schedule"),
        description=data.get("description", ""),
        times_run=data.get("times_run", 0),
        last_run=data.get("last_run", ""),
    )


def list_chains(*, skills_dir: Path | None = None) -> list[ChainInfo]:
    """List all saved chains."""
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    chains: list[ChainInfo] = []

    if not skills_dir.exists():
        return chains

    for skill_dir in sorted(skills_dir.iterdir()):
        if not skill_dir.is_dir():
            continue
        chain_file = skill_dir / "chain.json"
        if chain_file.exists():
            try:
                chains.append(load_chain(skill_dir.name, skills_dir=skills_dir))
            except (json.JSONDecodeError, KeyError, FileNotFoundError):
                continue

    return chains


def delete_chain(name: str, *, skills_dir: Path | None = None) -> None:
    """Delete a chain and its schedule.

    Raises:
        FileNotFoundError: If the chain doesn't exist.
    """
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    chain_dir = skills_dir / name

    if not chain_dir.exists() or not (chain_dir / "chain.json").exists():
        raise FileNotFoundError(
            f"No chain named '{name}' found.\n\n"
            f"See all chains with: alive chains"
        )

    # Remove crontab entry if present
    try:
        remove_schedule(name)
    except Exception:
        pass  # best effort

    # Remove from disk
    shutil.rmtree(chain_dir)

    # Remove from in-memory registry if loaded
    try:
        from alive.core.skill_registry import get_registry

        registry = get_registry()
        if name in registry._skills:
            del registry._skills[name]
    except Exception:
        pass


def is_chain(name: str, *, skills_dir: Path | None = None) -> bool:
    """Check whether a name refers to a chain."""
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    return (skills_dir / name / "chain.json").exists()


# ── Execution ───────────────────────────────────────────────────────────────


def run_chain(
    name: str,
    *,
    input_override: dict[str, Any] | None = None,
    skills_dir: Path | None = None,
    console: Console | None = None,
) -> ChainResult:
    """Execute a chain — run each skill in sequence, piping output → input.

    Args:
        name: Chain name.
        input_override: Override the chain's default_input.
        skills_dir: Override skills directory.
        console: Rich console for progress output.

    Returns:
        ChainResult with step-by-step results and final output.
    """
    from alive.config import SKILLS_DIR
    from alive.core.skill_registry import get_registry
    from alive.sandbox.runner import run_skill

    skills_dir = skills_dir or SKILLS_DIR
    console = console or Console()
    chain = load_chain(name, skills_dir=skills_dir)
    registry = get_registry()

    current_input = input_override if input_override is not None else chain.default_input
    step_results: list[ChainStepResult] = []
    total_start = time.monotonic()

    n = len(chain.steps)
    console.print(Panel(
        f"[bold]{name}[/]\n\n"
        f"[dim]{' → '.join(chain.steps)}[/]",
        title="[bold cyan]⛓  Running Chain[/]",
        border_style="cyan",
    ))

    for i, step_name in enumerate(chain.steps):
        step_label = f"Step {i + 1}/{n}: {step_name}"

        with console.status(f"[bold cyan]  {step_label}...", spinner="dots"):
            sr = run_skill(step_name, current_input, skills_dir=skills_dir)

        # Track stats on individual skill
        try:
            registry.update_stats(step_name, success=sr.success)
        except KeyError:
            pass

        step_result = ChainStepResult(
            skill_name=step_name,
            success=sr.success,
            output=sr.output,
            error=sr.error,
            duration_ms=sr.duration_ms,
        )
        step_results.append(step_result)

        if sr.success:
            console.print(f"  [green]✓[/green] {step_label}  [dim]({sr.duration_ms}ms)[/]")
            # Pipe output to next step
            current_input = sr.output or {}
        else:
            console.print(f"  [red]✗[/red] {step_label}")
            if sr.error:
                console.print(Panel(
                    sr.error[:500],
                    title=f"[bold red]Chain stopped at {step_name}[/]",
                    border_style="red",
                ))

            total_ms = int((time.monotonic() - total_start) * 1000)
            _update_chain_stats(name, success=False, skills_dir=skills_dir)

            return ChainResult(
                chain_name=name,
                step_results=step_results,
                final_output=None,
                success=False,
                total_duration_ms=total_ms,
            )

    total_ms = int((time.monotonic() - total_start) * 1000)
    _update_chain_stats(name, success=True, skills_dir=skills_dir)

    return ChainResult(
        chain_name=name,
        step_results=step_results,
        final_output=current_input,
        success=True,
        total_duration_ms=total_ms,
    )


def _update_chain_stats(name: str, *, success: bool, skills_dir: Path | None = None) -> None:
    """Update chain metadata with run stats."""
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    chain_path = skills_dir / name / "chain.json"
    if not chain_path.exists():
        return

    try:
        data = json.loads(chain_path.read_text())
        data["times_run"] = data.get("times_run", 0) + 1
        data["last_run"] = datetime.now(timezone.utc).isoformat()
        chain_path.write_text(json.dumps(data, indent=2) + "\n")

        # Also update metadata.json for registry stats
        meta_path = skills_dir / name / "metadata.json"
        if meta_path.exists():
            meta = json.loads(meta_path.read_text())
            meta["times_used"] = meta.get("times_used", 0) + 1
            if success:
                meta["success_count"] = meta.get("success_count", 0) + 1
            else:
                meta["fail_count"] = meta.get("fail_count", 0) + 1
            meta["last_used"] = datetime.now(timezone.utc).isoformat()
            meta_path.write_text(json.dumps(meta, indent=2) + "\n")
    except Exception:
        pass  # best effort


# ── Scheduling ──────────────────────────────────────────────────────────────


def install_schedule(name: str, cron_expr: str, *, skills_dir: Path | None = None) -> str:
    """Add a crontab entry for a chain.

    Returns:
        Human-readable schedule description.
    """
    from alive.config import LOGS_DIR

    LOGS_DIR.mkdir(parents=True, exist_ok=True)

    # Find alive executable
    alive_bin = shutil.which("alive")
    if not alive_bin:
        import sys
        alive_bin = f"{sys.executable} -m alive"

    log_path = LOGS_DIR / f"{name}.log"
    marker = f"# alive-chain:{name}"
    cron_line = f"{cron_expr} {alive_bin} run {name} >> {log_path} 2>&1"

    # Read existing crontab
    try:
        result = subprocess.run(
            ["crontab", "-l"], capture_output=True, text=True, timeout=10
        )
        existing = result.stdout if result.returncode == 0 else ""
    except (FileNotFoundError, subprocess.TimeoutExpired):
        existing = ""

    # Remove old entry for this chain
    lines = existing.splitlines()
    new_lines = []
    skip_next = False
    for line in lines:
        if skip_next:
            skip_next = False
            continue
        if line.strip() == marker:
            skip_next = True
            continue
        new_lines.append(line)

    # Add new entry
    new_lines.append(marker)
    new_lines.append(cron_line)

    # Write back
    new_crontab = "\n".join(new_lines) + "\n"
    try:
        proc = subprocess.run(
            ["crontab", "-"], input=new_crontab, capture_output=True,
            text=True, timeout=10,
        )
        if proc.returncode != 0:
            raise RuntimeError(f"crontab failed: {proc.stderr}")
    except FileNotFoundError:
        raise RuntimeError(
            "crontab not found. Schedule could not be installed.\n"
            "You can manually add this to your crontab:\n"
            f"  {cron_line}"
        )

    # Update chain.json
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    chain_path = skills_dir / name / "chain.json"
    if chain_path.exists():
        try:
            data = json.loads(chain_path.read_text())
            data["schedule"] = cron_expr
            chain_path.write_text(json.dumps(data, indent=2) + "\n")
        except Exception:
            pass

    return describe_cron(cron_expr)


def remove_schedule(name: str) -> None:
    """Remove the crontab entry for a chain."""
    try:
        result = subprocess.run(
            ["crontab", "-l"], capture_output=True, text=True, timeout=10
        )
        if result.returncode != 0:
            return
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return

    marker = f"# alive-chain:{name}"
    lines = result.stdout.splitlines()
    new_lines = []
    skip_next = False
    for line in lines:
        if skip_next:
            skip_next = False
            continue
        if line.strip() == marker:
            skip_next = True
            continue
        new_lines.append(line)

    new_crontab = "\n".join(new_lines) + "\n"
    try:
        subprocess.run(
            ["crontab", "-"], input=new_crontab, capture_output=True,
            text=True, timeout=10,
        )
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass


def describe_cron(cron_expr: str) -> str:
    """Convert a cron expression to a human-readable description."""
    parts = cron_expr.strip().split()
    if len(parts) != 5:
        return f"schedule: {cron_expr}"

    minute, hour, dom, month, dow = parts

    # Time string
    try:
        time_str = f"{int(hour):02d}:{int(minute):02d}"
    except ValueError:
        time_str = f"{hour}:{minute}"

    # Day-of-week patterns
    dow_map = {
        "*": "daily",
        "1-5": "weekdays",
        "0,6": "weekends",
        "1": "every Monday",
        "2": "every Tuesday",
        "3": "every Wednesday",
        "4": "every Thursday",
        "5": "every Friday",
        "6": "every Saturday",
        "0": "every Sunday",
        "7": "every Sunday",
    }

    if dom == "*" and month == "*":
        freq = dow_map.get(dow, f"cron dow={dow}")
        return f"{freq} at {time_str}"

    if dow == "*" and month == "*" and dom != "*":
        return f"day {dom} of every month at {time_str}"

    return f"schedule: {cron_expr}"


# ── Display helpers ─────────────────────────────────────────────────────────


def display_chain_created(chain: ChainInfo, schedule_desc: str | None = None, console: Console | None = None) -> None:
    """Display the chain creation panel."""
    console = console or Console()

    steps_line = " → ".join(chain.steps)
    lines = f"[bold]{chain.name}[/]\n\n{steps_line}"

    if schedule_desc:
        lines += f"\n[bold]Schedule:[/] {schedule_desc}"

    lines += f"\n\n[dim]Run now with:[/] alive run {chain.name}"

    console.print(Panel(
        lines,
        title="[bold green]Chain Created[/]",
        border_style="green",
        padding=(1, 2),
    ))


def display_chains(chains: list[ChainInfo], console: Console | None = None) -> None:
    """Display all chains in a rich table."""
    console = console or Console()

    if not chains:
        console.print(Panel(
            "[dim]No chains created yet.[/]\n\n"
            "[dim]Create one with:[/] alive chain my-pipeline skill1 skill2 ...",
            title="[bold]Chains[/]",
            border_style="yellow",
        ))
        return

    table = Table(title="Skill Chains", border_style="cyan", show_lines=False)
    table.add_column("Name", style="bold green", no_wrap=True)
    table.add_column("Steps", max_width=50)
    table.add_column("Schedule", style="dim")
    table.add_column("Runs", justify="right")
    table.add_column("Last Run", justify="right", style="dim")

    for c in chains:
        steps_str = " → ".join(c.steps)
        sched = describe_cron(c.schedule) if c.schedule else "[dim]none[/]"
        last = c.last_run.split("T")[0] if c.last_run else "never"
        table.add_row(c.name, steps_str, sched, str(c.times_run), last)

    console.print(table)
