"""
Lightweight pipeline executor for g-gremlin.

Executes YAML-defined steps sequentially with checkpointing/resume support.
Honors --apply: when apply=False, steps are planned but not executed.
"""

from __future__ import annotations

import json
import shlex
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

import click
import yaml

from g_gremlin.contracts import AgenticResult
from g_gremlin.safety import SafetyContext


@dataclass
class PipelineStep:
    name: str
    command: str
    params: Dict[str, Any] = field(default_factory=dict)
    depends_on: List[str] = field(default_factory=list)
    output: Optional[str] = None


def _load_pipeline_config(path: Path) -> Dict[str, Any]:
    if not path.exists():
        raise click.ClickException(f"Pipeline config not found: {path}")
    try:
        with path.open("r", encoding="utf-8") as handle:
            return yaml.safe_load(handle) or {}
    except Exception as exc:
        raise click.ClickException(f"Failed to read pipeline config: {exc}") from exc


def _validate_steps(raw_steps: Any) -> List[PipelineStep]:
    if not isinstance(raw_steps, list):
        raise click.ClickException("Pipeline config must include a 'steps' list.")
    steps: List[PipelineStep] = []
    for item in raw_steps:
        if not isinstance(item, dict):
            raise click.ClickException("Each step must be a mapping.")
        name = item.get("name")
        command = item.get("command")
        if not name or not command:
            raise click.ClickException("Each step requires 'name' and 'command'.")
        depends_raw = item.get("depends_on", []) or []
        if isinstance(depends_raw, str):
            depends = [depends_raw]
        elif isinstance(depends_raw, list):
            depends = depends_raw
        else:
            raise click.ClickException("depends_on must be a list or string if provided.")
        steps.append(
            PipelineStep(
                name=str(name),
                command=str(command),
                params=item.get("params", {}) or {},
                depends_on=depends,
                output=item.get("output"),
            )
        )
    return steps


def _checkpoint_path(config_path: Path) -> Path:
    return Path(".g_gremlin") / "checkpoints" / "pipeline" / f"{config_path.stem}.json"


def _load_checkpoint(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _save_checkpoint(path: Path, payload: Dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")


def _interpolate_value(value: Any, outputs: Dict[str, str], variables: Dict[str, Any]) -> Any:
    if not isinstance(value, str):
        return value
    text = value
    for step_name, out_path in outputs.items():
        text = text.replace(f"${{output.{step_name}}}", out_path)
        text = text.replace(f"{{{{{step_name}.output}}}}", out_path)
    for key, val in variables.items():
        replacement = str(val)
        text = text.replace(f"${{{key}}}", replacement)
        text = text.replace(f"{{{{{key}}}}}", replacement)
    return text


def _interpolate_params(params: Dict[str, Any], outputs: Dict[str, str], variables: Dict[str, Any]) -> Dict[str, Any]:
    resolved: Dict[str, Any] = {}
    for key, val in (params or {}).items():
        if isinstance(val, dict):
            resolved[key] = _interpolate_params(val, outputs, variables)
        elif isinstance(val, list):
            resolved[key] = [_interpolate_value(item, outputs, variables) for item in val]
        else:
            resolved[key] = _interpolate_value(val, outputs, variables)
    return resolved


def _build_command(step: PipelineStep, params: Dict[str, Any]) -> List[str]:
    parts = shlex.split(step.command)
    for key, val in params.items():
        flag = f"--{str(key).replace('_', '-')}"
        if isinstance(val, bool):
            if val:
                parts.append(flag)
            continue
        if isinstance(val, (list, tuple)):
            parts.append(flag)
            parts.append(",".join(str(v) for v in val))
        else:
            parts.extend([flag, str(val)])
    return parts


def _execute_step(
    step: PipelineStep,
    params: Dict[str, Any],
    workdir: Path,
) -> Dict[str, Any]:
    cmd = _build_command(step, params)
    try:
        proc = subprocess.run(
            cmd,
            cwd=str(workdir),
            check=False,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError as exc:
        raise click.ClickException(f"Command not found for step '{step.name}': {step.command}") from exc

    return {
        "return_code": proc.returncode,
        "stdout": proc.stdout.strip(),
        "stderr": proc.stderr.strip(),
        "command": cmd,
    }


def run_pipeline(config_path: Path, apply: bool) -> AgenticResult:
    """
    Execute a pipeline defined in YAML.

    Args:
        config_path: Path to pipeline YAML.
        apply: When False, steps are planned but not executed.

    Returns:
        AgenticResult summarizing the pipeline run.
    """
    cfg_path = Path(config_path)
    safety_ctx = SafetyContext(command="g-gremlin pipeline", remote_mutation=True)
    if apply:
        safety_ctx.enforce_apply(apply)

    config = _load_pipeline_config(cfg_path)
    pipeline_name = config.get("name") or cfg_path.stem
    variables = config.get("variables", {}) or {}
    steps = _validate_steps(config.get("steps"))

    checkpoint_file = _checkpoint_path(cfg_path)
    checkpoint = _load_checkpoint(checkpoint_file)
    completed = set(checkpoint.get("completed", []))
    outputs: Dict[str, str] = dict(checkpoint.get("outputs", {}))

    results: List[Dict[str, Any]] = []
    status = "success"
    error_msg: Optional[str] = None

    for step in steps:
        # Dependency validation
        missing = [dep for dep in step.depends_on if dep not in completed]
        if missing:
            raise click.ClickException(
                f"Step '{step.name}' depends on {', '.join(missing)} which have not completed."
            )

        resolved_params = _interpolate_params(step.params, outputs, variables)
        resolved_output = _interpolate_value(step.output, outputs, variables) if step.output else None
        if resolved_output:
            out_path = Path(resolved_output)
            if not out_path.is_absolute():
                out_path = (cfg_path.parent / out_path).resolve()
            resolved_output = str(out_path)

        if not apply:
            results.append(
                {
                    "name": step.name,
                    "status": "planned",
                    "command": step.command,
                    "params": resolved_params,
                    "output": resolved_output,
                }
            )
            completed.add(step.name)
            if resolved_output:
                outputs[step.name] = resolved_output
            continue

        if step.name in completed:
            results.append(
                {
                    "name": step.name,
                    "status": "skipped_resume",
                    "output": outputs.get(step.name),
                }
            )
            continue

        exec_result = _execute_step(step, resolved_params, cfg_path.parent)
        step_status = "success" if exec_result["return_code"] == 0 else "fail"
        if resolved_output:
            outputs[step.name] = resolved_output
        completed.add(step.name)

        checkpoint_payload = {
            "pipeline": pipeline_name,
            "completed": list(completed),
            "outputs": outputs,
        }
        _save_checkpoint(checkpoint_file, checkpoint_payload)

        results.append(
            {
                "name": step.name,
                "status": step_status,
                "output": resolved_output,
                "return_code": exec_result["return_code"],
                "stdout": exec_result["stdout"],
                "stderr": exec_result["stderr"],
            }
        )

        if step_status != "success":
            status = "fail"
            error_msg = f"Step '{step.name}' failed with code {exec_result['return_code']}"
            break

    if status == "success" and checkpoint_file.exists():
        checkpoint_file.unlink()

    result_payload = {
        "pipeline": pipeline_name,
        "config": str(cfg_path),
        "steps": results,
        "checkpoint": str(checkpoint_file),
    }

    return safety_ctx.build_result(
        status=status,
        dry_run=not apply,
        result=result_payload,
        error=error_msg,
    )
