#!/usr/bin/env python3
"""
planner.py - Echo Ginnie Planner & Reflex Brain (WO#GINNIE_06)
===============================================================

Turn work order prompts into structured plans with reflex safety checks.

The planner:
1. Classifies the request type
2. Generates a 3-7 step plan
3. Runs reflex checks to catch/repair unsafe steps
4. Provides deterministic, receipts-first execution
"""

import re
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Callable

from helix_cdc.ginnie.config import GinnieConfig


@dataclass
class GinnieStep:
    """A single step in a Ginnie plan."""
    kind: str  # "read", "edit", "run_cmd", "helix_tool", "symbolic_tool", "report"
    description: str
    target_paths: List[str] = field(default_factory=list)
    tool_name: Optional[str] = None
    args: Dict[str, Any] = field(default_factory=dict)
    priority: int = 1  # 1=normal, 0=critical, 2=optional

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class GinniePlan:
    """A complete plan for a work order."""
    wo_id: str
    raw_request: str
    request_type: str
    steps: List[GinnieStep] = field(default_factory=list)
    notes: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    reflex_applied: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            "wo_id": self.wo_id,
            "raw_request": self.raw_request,
            "request_type": self.request_type,
            "step_count": len(self.steps),
            "steps": [s.to_dict() for s in self.steps],
            "notes": self.notes,
            "warnings": self.warnings,
            "reflex_applied": self.reflex_applied,
        }


@dataclass
class PlanReflexConfig:
    """Configuration for plan reflex checks."""
    max_steps: int = 10
    forbid_rm_rf: bool = True
    forbid_sudo: bool = True
    require_tests_for_code_change: bool = True
    require_backup_for_edit: bool = False
    max_files_per_edit: int = 5


# Request classification patterns
REQUEST_PATTERNS = {
    "read_only": [
        r"\bread\b", r"\bshow\b", r"\blist\b", r"\bsummarize\b", r"\bexplain\b",
        r"\bwhat is\b", r"\bhow does\b", r"\bdescribe\b",
    ],
    "modify_code": [
        r"\badd\b", r"\bcreate\b", r"\bwrite\b", r"\bedit\b", r"\bmodify\b",
        r"\brefactor\b", r"\brename\b", r"\bfix\b", r"\bimplement\b",
    ],
    "run_bench": [
        r"\bperf\b", r"\bbench\b", r"\btest\b", r"\brun\b", r"\bexecute\b",
        r"\bsmoketest\b", r"\bgauntlet\b",
    ],
    "symbolic": [
        r"\bkrisper\b", r"\bbio.?poetica\b", r"\bsymbolic\b", r"\bentropy\b",
        r"\bscore\b", r"\bexpand\b",
    ],
    "helix": [
        r"\bhelix\b", r"\bjazz\b", r"\bseed\b", r"\bregen\b", r"\bcdna\b",
        r"\bfibpi\b", r"\bvalley\b", r"\bwave\b",
    ],
}


def classify_request(request: str) -> str:
    """
    Classify a work order request into a type.

    Returns: 'read_only', 'modify_code', 'run_bench', 'symbolic', 'helix', or 'mixed'
    """
    request_lower = request.lower()
    matches = {}

    for req_type, patterns in REQUEST_PATTERNS.items():
        count = sum(1 for p in patterns if re.search(p, request_lower))
        if count > 0:
            matches[req_type] = count

    if not matches:
        return "mixed"

    # If multiple types match, check for dominant
    if len(matches) > 1:
        sorted_matches = sorted(matches.items(), key=lambda x: x[1], reverse=True)
        if sorted_matches[0][1] > sorted_matches[1][1]:
            return sorted_matches[0][0]
        return "mixed"

    return list(matches.keys())[0]


def plan_work_order(
    request: str,
    wo_id: str,
    config: GinnieConfig,
    sandbox=None,
) -> GinniePlan:
    """
    Generate a plan for a work order.

    Uses heuristics to build a 3-7 step plan based on request type.
    """
    request_type = classify_request(request)

    plan = GinniePlan(
        wo_id=wo_id,
        raw_request=request,
        request_type=request_type,
    )

    # Build steps based on type
    if request_type == "read_only":
        plan.steps = _build_read_plan(request, config)
    elif request_type == "modify_code":
        plan.steps = _build_modify_plan(request, config)
    elif request_type == "run_bench":
        plan.steps = _build_bench_plan(request, config)
    elif request_type == "symbolic":
        plan.steps = _build_symbolic_plan(request, config)
    elif request_type == "helix":
        plan.steps = _build_helix_plan(request, config)
    else:  # mixed
        plan.steps = _build_mixed_plan(request, config)

    # Add report step if not present
    if plan.steps and plan.steps[-1].kind != "report":
        plan.steps.append(GinnieStep(
            kind="report",
            description="Summarize results and findings",
        ))

    return plan


def _build_read_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for read-only requests."""
    steps = []

    # Extract target file/dir from request
    targets = _extract_targets(request, config)

    if targets:
        steps.append(GinnieStep(
            kind="read",
            description=f"Read target file(s): {', '.join(targets[:3])}",
            target_paths=targets[:3],
        ))
    else:
        # Discovery step
        steps.append(GinnieStep(
            kind="helix_tool",
            description="Discover repository structure",
            tool_name="list_makefile_targets",
        ))

    steps.append(GinnieStep(
        kind="report",
        description="Analyze and summarize findings",
    ))

    return steps


def _build_modify_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for code modification requests."""
    steps = []
    targets = _extract_targets(request, config)

    # Step 1: Read current state
    if targets:
        steps.append(GinnieStep(
            kind="read",
            description=f"Read current state of: {', '.join(targets[:3])}",
            target_paths=targets[:3],
        ))
    else:
        steps.append(GinnieStep(
            kind="helix_tool",
            description="Discover relevant files",
            tool_name="list_makefile_targets",
        ))

    # Step 2: Analyze what to change
    steps.append(GinnieStep(
        kind="symbolic_tool",
        description="Score current code with KRISPER",
        tool_name="krisper_score",
    ))

    # Step 3: Make changes
    steps.append(GinnieStep(
        kind="edit",
        description=f"Apply changes: {request[:50]}...",
        target_paths=targets[:3] if targets else [],
    ))

    # Step 4: Run tests
    steps.append(GinnieStep(
        kind="helix_tool",
        description="Run smoke tests to verify changes",
        tool_name="healthcheck_quick",
    ))

    return steps


def _build_bench_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for benchmark/test requests."""
    steps = []

    # Determine which bench to run
    if "gauntlet" in request.lower():
        tool = "gauntlet"
    elif "perf" in request.lower():
        tool = "perf_suite"
    else:
        tool = "healthcheck_quick"

    steps.append(GinnieStep(
        kind="helix_tool",
        description=f"Run {tool}",
        tool_name=tool,
    ))

    steps.append(GinnieStep(
        kind="helix_tool",
        description="Collect Jazz metrics",
        tool_name="jazz_metrics",
    ))

    return steps


def _build_symbolic_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for symbolic/KRISPER requests."""
    steps = []
    targets = _extract_targets(request, config)

    if targets:
        steps.append(GinnieStep(
            kind="read",
            description=f"Read target: {targets[0]}",
            target_paths=targets[:1],
        ))

    # Determine which symbolic tool
    if "expand" in request.lower() or "poetica" in request.lower():
        steps.append(GinnieStep(
            kind="symbolic_tool",
            description="Expand with Bio_Poetica",
            tool_name="bio_poetica_expand",
        ))
    elif "entropy" in request.lower():
        steps.append(GinnieStep(
            kind="symbolic_tool",
            description="Measure symbolic entropy",
            tool_name="symbolic_entropy",
        ))
    else:
        steps.append(GinnieStep(
            kind="symbolic_tool",
            description="Score with KRISPER",
            tool_name="krisper_score",
        ))

        steps.append(GinnieStep(
            kind="symbolic_tool",
            description="Get KRISPER suggestions",
            tool_name="krisper_suggest",
        ))

    return steps


def _build_helix_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for Helix-specific requests."""
    steps = []

    if "seed" in request.lower():
        steps.append(GinnieStep(
            kind="helix_tool",
            description="List available seeds",
            tool_name="list_seeds",
        ))
    elif "regen" in request.lower():
        steps.append(GinnieStep(
            kind="helix_tool",
            description="Regenerate from seed",
            tool_name="regen_seed",
        ))
    elif "jazz" in request.lower():
        steps.append(GinnieStep(
            kind="helix_tool",
            description="Get Jazz metrics",
            tool_name="jazz_metrics",
        ))
    else:
        steps.append(GinnieStep(
            kind="helix_tool",
            description="Run Helix healthcheck",
            tool_name="healthcheck_quick",
        ))

    return steps


def _build_mixed_plan(request: str, config: GinnieConfig) -> List[GinnieStep]:
    """Build plan for mixed/unclear requests."""
    steps = []
    targets = _extract_targets(request, config)

    # Discovery
    steps.append(GinnieStep(
        kind="helix_tool",
        description="Discover repository state",
        tool_name="healthcheck_quick",
    ))

    # Read if targets
    if targets:
        steps.append(GinnieStep(
            kind="read",
            description=f"Read: {targets[0]}",
            target_paths=targets[:1],
        ))

    # Generic analysis
    steps.append(GinnieStep(
        kind="symbolic_tool",
        description="Analyze with KRISPER",
        tool_name="krisper_score",
    ))

    return steps


def _extract_targets(request: str, config: GinnieConfig) -> List[str]:
    """Extract file/directory targets from request."""
    targets = []

    # Common file patterns
    patterns = [
        r'(?:file|read|edit|modify|check)\s+[`"\']?([a-zA-Z0-9_/.]+\.[a-z]+)[`"\']?',
        r'[`"\']([a-zA-Z0-9_/]+\.(py|md|json|yaml|txt|sh))[`"\']',
        r'(?:in|from|to)\s+[`"\']?([a-zA-Z0-9_/]+\.[a-z]+)[`"\']?',
    ]

    for pattern in patterns:
        matches = re.findall(pattern, request, re.IGNORECASE)
        for match in matches:
            if isinstance(match, tuple):
                match = match[0]
            # Validate it looks like a path
            if "/" in match or match.endswith((".py", ".md", ".json", ".yaml", ".txt", ".sh")):
                targets.append(match)

    # Also check for "Makefile" specifically
    if "makefile" in request.lower() and "Makefile" not in targets:
        targets.append("Makefile")

    return list(set(targets))  # Dedupe


# ========== REFLEX BRAIN ==========

def sanity_check_plan(plan: GinniePlan, cfg: PlanReflexConfig) -> List[str]:
    """
    Check plan for safety issues.

    Returns list of warnings/errors.
    """
    warnings = []

    # Check step count
    if len(plan.steps) > cfg.max_steps:
        warnings.append(f"WARN: Plan has {len(plan.steps)} steps (max: {cfg.max_steps})")

    if len(plan.steps) == 0:
        warnings.append("ERROR: Plan has no steps")

    # Check for dangerous patterns
    for i, step in enumerate(plan.steps):
        desc_lower = step.description.lower()

        # Forbid rm -rf
        if cfg.forbid_rm_rf:
            if "rm -rf" in desc_lower or "rm -r" in desc_lower:
                warnings.append(f"ERROR: Step {i+1} contains dangerous rm command")

        # Forbid sudo
        if cfg.forbid_sudo:
            if "sudo" in desc_lower:
                warnings.append(f"WARN: Step {i+1} uses sudo")

        # Check paths
        for path in step.target_paths:
            if path.startswith("/") and not path.startswith("/home/"):
                if not path.startswith("/tmp/"):
                    warnings.append(f"WARN: Step {i+1} targets system path: {path}")

            if ".." in path:
                warnings.append(f"ERROR: Step {i+1} has path traversal: {path}")

    # Check for test step after code changes
    if cfg.require_tests_for_code_change:
        has_edit = any(s.kind == "edit" for s in plan.steps)
        has_test = any(
            s.kind in ("helix_tool", "run_cmd") and
            any(t in (s.tool_name or "").lower() or t in s.description.lower()
                for t in ["test", "check", "smoke", "health"])
            for s in plan.steps
        )
        if has_edit and not has_test:
            warnings.append("WARN: Plan edits code but has no test step")

    return warnings


def repair_plan_if_needed(plan: GinniePlan, cfg: PlanReflexConfig) -> GinniePlan:
    """
    Repair unsafe plan by removing/modifying dangerous steps.
    """
    repaired_steps = []
    removed = 0
    added_test = False

    has_edit = any(s.kind == "edit" for s in plan.steps)

    for step in plan.steps:
        desc_lower = step.description.lower()

        # Skip dangerous rm commands
        if cfg.forbid_rm_rf and ("rm -rf" in desc_lower or "rm -r" in desc_lower):
            removed += 1
            plan.notes.append(f"Removed dangerous step: {step.description[:50]}")
            continue

        # Remove sudo if forbidden
        if cfg.forbid_sudo and "sudo" in desc_lower:
            step.description = step.description.replace("sudo ", "")
            plan.notes.append("Removed sudo from step")

        # Clean path traversal
        step.target_paths = [p for p in step.target_paths if ".." not in p]

        repaired_steps.append(step)

    # Add test step if missing after edit
    if cfg.require_tests_for_code_change and has_edit:
        has_test = any(
            s.kind in ("helix_tool", "run_cmd") and
            "test" in (s.tool_name or "").lower() or "check" in s.description.lower()
            for s in repaired_steps
        )
        if not has_test:
            # Insert test step before report
            test_step = GinnieStep(
                kind="helix_tool",
                description="Verify changes with healthcheck",
                tool_name="healthcheck_quick",
            )
            # Insert before last step if it's a report
            if repaired_steps and repaired_steps[-1].kind == "report":
                repaired_steps.insert(-1, test_step)
            else:
                repaired_steps.append(test_step)
            added_test = True
            plan.notes.append("Added test step after code modification")

    plan.steps = repaired_steps
    plan.reflex_applied = True

    if removed > 0:
        plan.notes.append(f"Reflex removed {removed} unsafe step(s)")
    if added_test:
        plan.notes.append("Reflex added test verification step")

    return plan


def apply_reflex(plan: GinniePlan, cfg: Optional[PlanReflexConfig] = None) -> GinniePlan:
    """
    Apply reflex brain to a plan.

    Checks for issues and repairs if needed.
    """
    cfg = cfg or PlanReflexConfig()

    warnings = sanity_check_plan(plan, cfg)
    plan.warnings = warnings

    # Only repair if there are issues
    if any(w.startswith("ERROR") or w.startswith("WARN") for w in warnings):
        plan = repair_plan_if_needed(plan, cfg)

    return plan
