"""Epic management MCP tools for SPOQ."""

import json
import shutil
from datetime import datetime, timezone
from pathlib import Path

from spoq_server.server import mcp, project_root
from spoq_server import epic_utils
from spoq_server import roadmap_utils


def _resolve_epic_path(epic_path: str) -> Path:
    """Resolve epic path relative to project root."""
    p = Path(epic_path)
    if not p.is_absolute():
        p = project_root() / p
    return p


@mcp.tool()
def spoq_parse_epic(epic_path: str) -> str:
    """Parse an epic directory and return its metadata, tasks, and stats as JSON.

    Args:
        epic_path: Path to epic directory (absolute or relative to project root).
                   Example: "spoq/epics/active/spoq-mcp-server"
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)
        result = eu.parse_epic(resolved)
        # Remove _source_file from tasks for cleaner output
        for task in result.get("tasks", []):
            task.pop("_source_file", None)
        return json.dumps(result, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_validate_epic(epic_path: str, strict: bool = False) -> str:
    """Validate an epic's structure, dependencies, and phases.

    Args:
        epic_path: Path to epic directory.
        strict: If true, warnings become errors.

    Returns JSON with errors[], warnings[], and summary.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)

        if not resolved.exists():
            return json.dumps({"error": f"Epic directory not found: {resolved}"})

        errors = []
        warnings = []

        # Validate EPIC.md structure
        md_errors = eu.validate_epic_md_structure(resolved)
        # Treat "recommend" messages as warnings unless strict
        for err in md_errors:
            if "recommend" in err.lower() and not strict:
                warnings.append(err)
            else:
                errors.append(err)

        # Load and validate tasks
        tasks = eu.load_tasks(resolved)
        valid_tasks = [t for t in tasks if "error" not in t]
        error_tasks = [t for t in tasks if "error" in t]

        for t in error_tasks:
            errors.append(t["error"])

        for task in valid_tasks:
            source = Path(task.get("_source_file", "unknown"))
            task_errors = eu.validate_task_yaml_structure(task, source)
            errors.extend(task_errors)

        # Validate dependencies
        errors.extend(eu.validate_dependencies(valid_tasks))

        # Detect cycles
        cycle_errors = eu.detect_cycles(valid_tasks)
        errors.extend(cycle_errors)

        # Validate phases
        phase_errors = eu.validate_phases(valid_tasks)
        if strict:
            errors.extend(phase_errors)
        else:
            warnings.extend(phase_errors)

        summary = {
            "valid": len(errors) == 0,
            "total_tasks": len(tasks),
            "valid_tasks": len(valid_tasks),
            "error_count": len(errors),
            "warning_count": len(warnings),
        }

        return json.dumps({"errors": errors, "warnings": warnings, "summary": summary})
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_compute_waves(epic_path: str, critical_path: bool = False) -> str:
    """Compute execution waves from task dependency graph.

    Args:
        epic_path: Path to epic directory.
        critical_path: Include critical path analysis in output.

    Returns JSON with waves[], each containing task details.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)
        result = eu.parse_epic(resolved)

        if "error" in result:
            return json.dumps(result)

        tasks = result["tasks"]
        if not tasks:
            return json.dumps({"error": "No tasks found in epic"})

        waves_map = eu.compute_waves(tasks)
        wave_groups = eu.group_waves(waves_map)
        task_map = {t["id"]: t for t in tasks}

        waves = []
        for wave_num in sorted(wave_groups.keys()):
            task_ids = wave_groups[wave_num]
            wave_tasks = []
            for tid in sorted(task_ids):
                task = task_map.get(tid, {})
                wave_tasks.append({
                    "id": tid,
                    "title": task.get("title", tid),
                    "status": task.get("status", "pending"),
                    "priority": task.get("priority", "medium"),
                    "dependencies": task.get("dependencies", []),
                })
            waves.append({
                "wave": wave_num,
                "task_count": len(wave_tasks),
                "tasks": wave_tasks,
            })

        output = {
            "epic_name": result.get("epic_name", "unknown"),
            "total_waves": len(waves),
            "total_tasks": len(tasks),
            "waves": waves,
        }

        if critical_path:
            cp_tasks, cp_duration = eu.compute_critical_path(tasks, waves_map)
            output["critical_path"] = {
                "tasks": cp_tasks,
                "duration_hours": cp_duration,
                "length": len(cp_tasks),
            }

        return json.dumps(output, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_get_wave_tasks(epic_path: str, wave: int) -> str:
    """Get tasks for a specific wave number.

    Args:
        epic_path: Path to epic directory.
        wave: Wave number (0-indexed).

    Returns JSON with the wave's tasks only.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)
        result = eu.parse_epic(resolved)

        if "error" in result:
            return json.dumps(result)

        tasks = result["tasks"]
        waves_map = eu.compute_waves(tasks)
        wave_groups = eu.group_waves(waves_map)

        if wave not in wave_groups:
            available = sorted(wave_groups.keys())
            return json.dumps({
                "error": f"Wave {wave} not found. Available waves: {available}"
            })

        task_map = {t["id"]: t for t in tasks}
        wave_tasks = []
        for tid in sorted(wave_groups[wave]):
            task = task_map.get(tid, {})
            wave_tasks.append({
                "id": tid,
                "title": task.get("title", tid),
                "status": task.get("status", "pending"),
                "dependencies": task.get("dependencies", []),
                "files_to_touch": task.get("files_to_touch", []),
                "skills_required": task.get("skills_required", []),
            })

        return json.dumps({
            "wave": wave,
            "task_count": len(wave_tasks),
            "tasks": wave_tasks,
        })
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_generate_execution_plan(epic_path: str, include_critical_path: bool = True) -> str:
    """Generate a full execution plan with actionable task filtering.

    Computes waves from the dependency graph and marks each task as actionable
    (pending, in_progress, or rework) or non-actionable (completed, failed, qa).
    Essential for --resume workflows where completed tasks should be skipped.

    Used by both /agent-execution and /team-execution as the primary planning step.

    Args:
        epic_path: Path to epic directory.
        include_critical_path: Include critical path analysis (default True).

    Returns JSON with waves[], actionable counts, and per-task actionable flags.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)
        result = eu.parse_epic(resolved)

        if "error" in result:
            return json.dumps(result)

        tasks = result["tasks"]
        if not tasks:
            return json.dumps({"error": "No tasks found in epic"})

        actionable_statuses = {"pending", "rework", "in_progress"}

        waves_map = eu.compute_waves(tasks)
        wave_groups = eu.group_waves(waves_map)
        task_map = {t["id"]: t for t in tasks}

        waves = []
        total_actionable = 0
        for wave_num in sorted(wave_groups.keys()):
            task_ids = wave_groups[wave_num]
            wave_tasks = []
            wave_actionable = 0
            for tid in sorted(task_ids):
                task = task_map.get(tid, {})
                is_actionable = task.get("status", "pending") in actionable_statuses
                if is_actionable:
                    wave_actionable += 1
                wave_tasks.append({
                    "id": tid,
                    "title": task.get("title", tid),
                    "status": task.get("status", "pending"),
                    "priority": task.get("priority", "medium"),
                    "dependencies": task.get("dependencies", []),
                    "files_to_touch": task.get("files_to_touch", []),
                    "skills_required": task.get("skills_required", []),
                    "actionable": is_actionable,
                })
            total_actionable += wave_actionable
            waves.append({
                "wave": wave_num,
                "total_tasks": len(wave_tasks),
                "actionable_tasks": wave_actionable,
                "tasks": wave_tasks,
            })

        output = {
            "epic_name": result.get("epic_name", "unknown"),
            "total_waves": len(waves),
            "total_tasks": len(tasks),
            "actionable_tasks": total_actionable,
            "waves": waves,
        }

        if include_critical_path:
            cp_tasks, cp_duration = eu.compute_critical_path(tasks, waves_map)
            output["critical_path"] = {
                "tasks": [
                    {
                        "id": tid,
                        "title": task_map.get(tid, {}).get("title", tid),
                        "wave": waves_map.get(tid, -1),
                    }
                    for tid in cp_tasks
                ],
                "duration_hours": cp_duration,
                "length": len(cp_tasks),
            }

        return json.dumps(output, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_analyze_dag(epic_path: str, agents: int = 4) -> str:
    """Analyze the dependency DAG for critical path and parallelism metrics.

    Args:
        epic_path: Path to epic directory.
        agents: Number of parallel agents for effort estimation.

    Returns JSON with critical_path, parallelism, and effort analysis.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)

        tasks = eu.load_tasks(resolved)
        valid_tasks = [t for t in tasks if "error" not in t]

        if not valid_tasks:
            return json.dumps({"error": "No valid tasks found"})

        waves = eu.compute_waves(valid_tasks)
        wave_groups = eu.group_waves(waves)
        cp_tasks, cp_duration = eu.compute_critical_path(valid_tasks, waves)
        parallelism = eu.compute_parallelism_metrics(valid_tasks, waves)

        task_map = {t["id"]: t for t in valid_tasks}
        total_effort = sum(eu.get_task_duration(t) for t in valid_tasks)
        total_pert = sum(eu.calculate_pert_estimate(t) for t in valid_tasks)
        parallel_estimate = (
            max(cp_duration, total_effort / agents)
            if agents > 0
            else total_effort
        )
        speedup = total_effort / cp_duration if cp_duration > 0 else 1.0

        return json.dumps({
            "epic_name": resolved.name,
            "total_tasks": len(valid_tasks),
            "critical_path": {
                "tasks": cp_tasks,
                "duration_hours": cp_duration,
                "task_details": [
                    {
                        "id": tid,
                        "title": task_map.get(tid, {}).get("title", tid),
                        "duration": eu.get_task_duration(task_map.get(tid, {})),
                    }
                    for tid in cp_tasks
                ],
            },
            "parallelism": {
                "wave_count": parallelism["phase_count"],
                "max_parallelism": parallelism["max_parallelism"],
                "wave0_tasks": parallelism["phase1_tasks"],
                "wave0_ratio": parallelism["phase1_ratio"],
                "waves": {str(k): v for k, v in parallelism["phases"].items()},
            },
            "effort": {
                "total_hours": total_effort,
                "total_pert_hours": total_pert,
                "critical_path_hours": cp_duration,
                "parallel_estimate_hours": parallel_estimate,
                "speedup_factor": speedup,
                "num_agents": agents,
            },
        }, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_estimate_effort(epic_path: str) -> str:
    """Calculate PERT estimates per wave and total for an epic.

    Args:
        epic_path: Path to epic directory.

    Returns JSON with per-wave and total effort estimates.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)

        tasks = eu.load_tasks(resolved)
        valid_tasks = [t for t in tasks if "error" not in t]

        if not valid_tasks:
            return json.dumps({"error": "No valid tasks found"})

        waves = eu.compute_waves(valid_tasks)
        wave_groups = eu.group_waves(waves)
        task_map = {t["id"]: t for t in valid_tasks}

        wave_estimates = {}
        total_pert = 0

        for wave_num in sorted(wave_groups.keys()):
            task_ids = wave_groups[wave_num]
            tasks_detail = []
            wave_pert = 0

            for tid in task_ids:
                task = task_map.get(tid, {})
                estimate = task.get("estimate", {})
                opt = eu.parse_duration(estimate.get("optimistic", "30m"))
                real = eu.parse_duration(estimate.get("realistic", "1h"))
                pess = eu.parse_duration(estimate.get("pessimistic", "2h"))
                pert = eu.calculate_pert_estimate(task)
                wave_pert += pert

                tasks_detail.append({
                    "id": tid,
                    "title": task.get("title", tid),
                    "optimistic": opt,
                    "realistic": real,
                    "pessimistic": pess,
                    "pert": pert,
                })

            max_pert = (
                max(t["pert"] for t in tasks_detail) if tasks_detail else 0
            )
            wave_estimates[wave_num] = {
                "task_count": len(task_ids),
                "total_pert": wave_pert,
                "parallel_duration": max_pert,
                "tasks": tasks_detail,
            }
            total_pert += wave_pert

        parallel_duration = sum(
            w["parallel_duration"] for w in wave_estimates.values()
        )

        return json.dumps({
            "epic_name": resolved.name,
            "total_tasks": len(valid_tasks),
            "wave_count": len(wave_groups),
            "estimates": {
                "total_pert_hours": total_pert,
                "sequential_hours": total_pert,
                "parallel_hours": parallel_duration,
                "speedup_factor": (
                    total_pert / parallel_duration
                    if parallel_duration > 0
                    else 1
                ),
            },
            "waves": {str(k): v for k, v in wave_estimates.items()},
        }, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_complete_epic(
    epic_path: str,
    map_path: str = "",
    map_status: str = "",
    force: bool = False,
) -> str:
    """Archive a completed epic by moving it from active/ to complete/.

    Verifies every task has status "completed" (unless force=True), then
    moves the epic directory into the sibling ``complete/`` directory.
    When *map_path* is provided, the epic's **Status:** line in the map
    file is updated as well.

    Args:
        epic_path: Path to epic directory (e.g. "spoq/epics/active/my-epic").
        map_path: Optional path to map directory or .md file whose epic
                  status should be updated.
        map_status: Status text written to the map. Defaults to
                    "Complete ({done}/{total} tasks, {date})".
        force: Move the epic even if not all tasks are completed.

    Returns JSON with the new path, task summary, and optional map update.
    """
    try:
        eu = epic_utils
        resolved = _resolve_epic_path(epic_path)

        if not resolved.exists():
            return json.dumps({"error": f"Epic directory not found: {resolved}"})
        if not resolved.is_dir():
            return json.dumps({"error": f"Not a directory: {resolved}"})

        # --- verify completion ------------------------------------------------
        tasks = eu.load_tasks(resolved)
        valid = [t for t in tasks if "error" not in t]
        total = len(valid)
        done = sum(1 for t in valid if t.get("status") == "completed")

        if not force and done != total:
            incomplete = [
                {"id": t["id"], "status": t.get("status", "pending")}
                for t in valid
                if t.get("status") != "completed"
            ]
            return json.dumps({
                "error": f"{total - done} of {total} tasks are not completed. "
                         "Set force=True to move anyway.",
                "incomplete_tasks": incomplete,
            })

        # --- compute destination ----------------------------------------------
        parent = resolved.parent            # e.g. .../epics/active
        grandparent = parent.parent         # e.g. .../epics
        complete_dir = grandparent / "complete"
        destination = complete_dir / resolved.name

        if destination.exists():
            return json.dumps({
                "error": f"Destination already exists: {destination}",
            })

        complete_dir.mkdir(parents=True, exist_ok=True)
        shutil.move(str(resolved), str(destination))

        today = datetime.now(timezone.utc).strftime("%Y-%m-%d")

        # --- optional map update ----------------------------------------------
        map_result = None
        if map_path:
            from spoq_server import map_utils

            resolved_map = Path(map_path)
            if not resolved_map.is_absolute():
                resolved_map = project_root() / map_path

            status_text = map_status or f"Complete ({done}/{total} tasks, {today})"
            map_result = map_utils.update_epic_status_in_map(
                resolved_map, resolved.name, status_text,
            )

        # --- ROADMAP.yml update -----------------------------------------------
        roadmap_updated = False
        try:
            spoq_dir = str(project_root() / "spoq")
            data = roadmap_utils.load_roadmap(spoq_dir)
            entry = roadmap_utils.find_entry(data, resolved.name)
            if entry is not None:
                roadmap_utils.update_entry_tier(data, resolved.name, "complete")
                roadmap_utils.save_roadmap(spoq_dir, data)
                roadmap_updated = True
        except Exception:
            roadmap_updated = False

        return json.dumps({
            "epic_slug": resolved.name,
            "old_path": str(resolved),
            "new_path": str(destination),
            "tasks_completed": done,
            "tasks_total": total,
            "date": today,
            "map_update": map_result,
            "roadmap_updated": roadmap_updated,
        })
    except Exception as e:
        return json.dumps({"error": str(e)})


# =============================================================================
# Lifecycle tools: list, promote, demote
# =============================================================================

TIER_ORDER = ["backlog", "active", "complete"]


@mcp.tool()
def spoq_list_epics(tier: str = "", include_stats: bool = False) -> str:
    """List epics across backlog, active, and complete tiers.

    Scans spoq/epics/{backlog,active,complete} directories and returns
    slugs grouped by tier with optional task-level statistics.

    Args:
        tier: Filter to a single tier ("backlog", "active", or "complete").
              Empty string returns all tiers.
        include_stats: When True, load task YAMLs to compute progress
                       (tasks_total, tasks_completed, progress).

    Returns JSON with {backlog: [], active: [], complete: [], counts: {}}.
    """
    try:
        import yaml

        base = project_root() / "spoq" / "epics"
        tiers_to_scan = [tier] if tier else TIER_ORDER

        result: dict = {t: [] for t in TIER_ORDER}

        for t in tiers_to_scan:
            if t not in TIER_ORDER:
                return json.dumps({"error": f"Invalid tier '{t}'. Must be one of {TIER_ORDER}"})
            tier_dir = base / t
            if not tier_dir.is_dir():
                continue
            for child in sorted(tier_dir.iterdir()):
                if not child.is_dir():
                    continue
                slug = child.name
                if include_stats:
                    tasks_dir = child / "tasks"
                    tasks_total = 0
                    tasks_completed = 0
                    if tasks_dir.is_dir():
                        for yml in tasks_dir.glob("*.yml"):
                            try:
                                task_data = yaml.safe_load(yml.read_text())
                                if isinstance(task_data, dict):
                                    tasks_total += 1
                                    if task_data.get("status") == "completed":
                                        tasks_completed += 1
                            except Exception:
                                tasks_total += 1
                    progress = tasks_completed / tasks_total if tasks_total > 0 else 0.0
                    result[t].append({
                        "slug": slug,
                        "tasks_total": tasks_total,
                        "tasks_completed": tasks_completed,
                        "progress": round(progress, 2),
                    })
                else:
                    result[t].append(slug)

        counts = {t: len(result[t]) for t in TIER_ORDER}
        counts["total"] = sum(counts[t] for t in TIER_ORDER)
        result["counts"] = counts

        return json.dumps(result, default=str)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_promote(path: str, resource_type: str = "epic") -> str:
    """Promote an epic or map to the next lifecycle tier.

    Moves a resource directory from its current tier to the next one
    in the chain: backlog -> active -> complete.

    Args:
        path: Path to the epic or map directory (absolute or relative).
        resource_type: Either "epic" or "map".

    Returns JSON with {old_path, new_path, old_tier, new_tier, slug, roadmap_updated}.
    """
    try:
        resolved = Path(path)
        if not resolved.is_absolute():
            resolved = project_root() / path

        if not resolved.exists():
            return json.dumps({"error": f"Path not found: {resolved}"})
        if not resolved.is_dir():
            return json.dumps({"error": f"Not a directory: {resolved}"})

        # Detect current tier from path segments
        parts = resolved.parts
        current_tier = None
        for segment in parts:
            if segment in TIER_ORDER:
                current_tier = segment
                break

        if current_tier is None:
            return json.dumps({
                "error": f"Cannot determine tier from path. "
                         f"Expected one of {TIER_ORDER} in path segments."
            })

        current_index = TIER_ORDER.index(current_tier)
        if current_index >= len(TIER_ORDER) - 1:
            return json.dumps({
                "error": f"Already at highest tier '{current_tier}'. Cannot promote further."
            })

        next_tier = TIER_ORDER[current_index + 1]
        slug = resolved.name

        base = project_root() / ("spoq/epics" if resource_type == "epic" else "spoq/maps")
        new_path = base / next_tier / slug

        if new_path.exists():
            return json.dumps({
                "error": f"Destination already exists: {new_path}"
            })

        warnings = []
        if resource_type == "epic" and current_tier == "active" and next_tier == "complete":
            warnings.append(
                "Consider using spoq_complete_epic instead for active->complete "
                "promotion, as it validates task completion status."
            )

        new_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(str(resolved), str(new_path))

        # Attempt roadmap update (non-fatal)
        roadmap_updated = False
        try:
            spoq_dir = str(project_root() / "spoq")
            data = roadmap_utils.load_roadmap(spoq_dir)
            if roadmap_utils.update_entry_tier(data, slug, next_tier):
                roadmap_utils.save_roadmap(spoq_dir, data)
                roadmap_updated = True
        except Exception:
            pass

        result = {
            "old_path": str(resolved),
            "new_path": str(new_path),
            "old_tier": current_tier,
            "new_tier": next_tier,
            "slug": slug,
            "roadmap_updated": roadmap_updated,
        }
        if warnings:
            result["warnings"] = warnings

        return json.dumps(result)
    except Exception as e:
        return json.dumps({"error": str(e)})


@mcp.tool()
def spoq_demote(path: str, resource_type: str = "epic") -> str:
    """Demote an epic or map to the previous lifecycle tier.

    Moves a resource directory from its current tier to the previous one
    in the chain: complete -> active -> backlog.

    Args:
        path: Path to the epic or map directory (absolute or relative).
        resource_type: Either "epic" or "map".

    Returns JSON with {old_path, new_path, old_tier, new_tier, slug, roadmap_updated}.
    """
    try:
        resolved = Path(path)
        if not resolved.is_absolute():
            resolved = project_root() / path

        if not resolved.exists():
            return json.dumps({"error": f"Path not found: {resolved}"})
        if not resolved.is_dir():
            return json.dumps({"error": f"Not a directory: {resolved}"})

        # Detect current tier from path segments
        parts = resolved.parts
        current_tier = None
        for segment in parts:
            if segment in TIER_ORDER:
                current_tier = segment
                break

        if current_tier is None:
            return json.dumps({
                "error": f"Cannot determine tier from path. "
                         f"Expected one of {TIER_ORDER} in path segments."
            })

        current_index = TIER_ORDER.index(current_tier)
        if current_index <= 0:
            return json.dumps({
                "error": f"Already at lowest tier '{current_tier}'. Cannot demote further."
            })

        prev_tier = TIER_ORDER[current_index - 1]
        slug = resolved.name

        base = project_root() / ("spoq/epics" if resource_type == "epic" else "spoq/maps")
        new_path = base / prev_tier / slug

        if new_path.exists():
            return json.dumps({
                "error": f"Destination already exists: {new_path}"
            })

        new_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(str(resolved), str(new_path))

        # Attempt roadmap update (non-fatal)
        roadmap_updated = False
        try:
            spoq_dir = str(project_root() / "spoq")
            data = roadmap_utils.load_roadmap(spoq_dir)
            if roadmap_utils.update_entry_tier(data, slug, prev_tier):
                roadmap_utils.save_roadmap(spoq_dir, data)
                roadmap_updated = True
        except Exception:
            pass

        return json.dumps({
            "old_path": str(resolved),
            "new_path": str(new_path),
            "old_tier": current_tier,
            "new_tier": prev_tier,
            "slug": slug,
            "roadmap_updated": roadmap_updated,
        })
    except Exception as e:
        return json.dumps({"error": str(e)})
