"""
Coding task execution — plan, confirm, execute, backup/undo/diff.
"""

import json
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict

from elgringo.cli.colors import Colors


# ── Per-Project Config ───────────────────────────────────────────────

_ELGRINGO_DIR = ".elgringo"
_BACKUP_FILE = os.path.join(_ELGRINGO_DIR, "last_change.json")
_CONFIG_FILE = ".elgringo.yml"


def load_project_config() -> dict:
    """Load .elgringo.yml from current directory if it exists."""
    if not os.path.isfile(_CONFIG_FILE):
        return {}
    try:
        import yaml
        with open(_CONFIG_FILE, "r") as f:
            return yaml.safe_load(f) or {}
    except ImportError:
        config = {}
        try:
            with open(_CONFIG_FILE, "r") as f:
                for line in f:
                    line = line.strip()
                    if ":" in line and not line.startswith("#"):
                        k, v = line.split(":", 1)
                        config[k.strip()] = v.strip()
        except Exception:
            pass
        return config
    except Exception:
        return {}


# ── Backup Persistence (for undo/diff) ──────────────────────────────

def save_backup(project_path: str, backups: Dict[str, str], files_changed: list):
    """Save backup state to .elgringo/last_change.json for undo/diff."""
    elgringo_dir = os.path.join(project_path, _ELGRINGO_DIR)
    os.makedirs(elgringo_dir, exist_ok=True)
    backup_file = os.path.join(project_path, _BACKUP_FILE)

    data = {
        "project_path": project_path,
        "timestamp": datetime.now().isoformat(),
        "backups": backups,
        "files_changed": [
            {"path": fc.path, "action": fc.action, "lines_changed": fc.lines_changed}
            for fc in files_changed
        ],
    }
    with open(backup_file, "w") as f:
        json.dump(data, f, indent=2)

    # Add .elgringo/ to .gitignore if not already there
    gitignore = os.path.join(project_path, ".gitignore")
    if os.path.isfile(gitignore):
        content = open(gitignore).read()
        if ".elgringo/" not in content:
            with open(gitignore, "a") as f:
                f.write("\n.elgringo/\n")
    elif os.path.isdir(os.path.join(project_path, ".git")):
        with open(gitignore, "w") as f:
            f.write(".elgringo/\n")


def load_backup(project_path: str = None) -> dict:
    """Load the last backup state."""
    project_path = project_path or os.getcwd()
    backup_file = os.path.join(project_path, _BACKUP_FILE)
    if not os.path.isfile(backup_file):
        return {}
    try:
        with open(backup_file, "r") as f:
            return json.load(f)
    except Exception:
        return {}


def run_undo():
    """Restore files from the last coding change."""

    backup = load_backup()
    if not backup or not backup.get("backups"):
        print(f"{Colors.YELLOW}No backup found. Nothing to undo.{Colors.RESET}")
        return

    backups = backup["backups"]
    project = backup.get("project_path", os.getcwd())
    ts = backup.get("timestamp", "unknown")

    print(f"\n{Colors.BOLD}Undo last change{Colors.RESET} ({ts})")
    print(f"  Project: {project}")
    print(f"  Files to restore: {len(backups)}\n")

    for filepath in backups:
        abs_path = os.path.join(project, filepath) if not os.path.isabs(filepath) else filepath
        current = ""
        if os.path.isfile(abs_path):
            current = open(abs_path, "r", errors="replace").read()
        original = backups[filepath]
        if current == original:
            print(f"  {Colors.DIM}= {filepath} (unchanged){Colors.RESET}")
        else:
            print(f"  {Colors.YELLOW}↩ {filepath}{Colors.RESET}")

    confirm = input("\n  Restore these files? [y/N] ").strip().lower()
    if confirm != "y":
        print(f"  {Colors.DIM}Cancelled.{Colors.RESET}")
        return

    restored = 0
    for filepath, content in backups.items():
        abs_path = os.path.join(project, filepath) if not os.path.isabs(filepath) else filepath
        try:
            Path(abs_path).write_text(content)
            restored += 1
        except Exception as e:
            print(f"  {Colors.RED}Failed: {filepath}: {e}{Colors.RESET}")

    print(f"\n  {Colors.GREEN}Restored {restored}/{len(backups)} files.{Colors.RESET}\n")

    # Remove the backup file
    backup_file = os.path.join(project, _BACKUP_FILE)
    if os.path.isfile(backup_file):
        os.remove(backup_file)


def run_diff():
    """Show diff of the last coding change."""
    import difflib

    backup = load_backup()
    if not backup or not backup.get("backups"):
        print(f"{Colors.YELLOW}No recent changes to show.{Colors.RESET}")
        return

    backups = backup["backups"]
    project = backup.get("project_path", os.getcwd())
    ts = backup.get("timestamp", "unknown")

    print(f"\n{Colors.BOLD}Last change{Colors.RESET} ({ts})")
    print(f"  Project: {project}\n")

    has_diff = False
    for filepath, original in backups.items():
        abs_path = os.path.join(project, filepath) if not os.path.isabs(filepath) else filepath
        if os.path.isfile(abs_path):
            current = open(abs_path, "r", errors="replace").read()
        else:
            current = ""

        if current == original:
            continue

        has_diff = True
        diff = difflib.unified_diff(
            original.splitlines(keepends=True),
            current.splitlines(keepends=True),
            fromfile=f"a/{filepath}",
            tofile=f"b/{filepath}",
        )
        print(f"{Colors.BOLD}--- {filepath} ---{Colors.RESET}")
        for line in diff:
            if line.startswith("+") and not line.startswith("+++"):
                print(f"{Colors.GREEN}{line}{Colors.RESET}", end="")
            elif line.startswith("-") and not line.startswith("---"):
                print(f"{Colors.RED}{line}{Colors.RESET}", end="")
            else:
                print(line, end="")
        print()

    if not has_diff:
        print(f"  {Colors.DIM}No differences found (files may have been restored).{Colors.RESET}\n")


async def run_coding_task(
    task: str,
    files: list = None,
    dry_run: bool = False,
    auto_commit: bool = False,
    test_cmd: str = "",
    no_tests: bool = False,
    yes: bool = False,
):
    """Run a coding task that reads, edits, and writes files.

    Flow: plan (dry-run) → show plan → confirm → execute → save backup.
    """
    import time as _time

    project_path = os.getcwd()

    # Load per-project config
    config = load_project_config()
    if not test_cmd and config.get("test_command"):
        test_cmd = config["test_command"]

    print(f"\n{Colors.BOLD}{'=' * 60}{Colors.RESET}")
    print(f"{Colors.CYAN}  EL GRINGO — CODING AGENT{Colors.RESET}")
    print(f"{Colors.BOLD}{'=' * 60}{Colors.RESET}")
    print(f"\n  {Colors.DIM}Project: {project_path}{Colors.RESET}")
    if config:
        print(f"  {Colors.DIM}Config: .elgringo.yml loaded{Colors.RESET}")
    if dry_run:
        print(f"  {Colors.YELLOW}DRY RUN — planning only, no changes{Colors.RESET}")
    print(f"\n  {Colors.GREEN}Task:{Colors.RESET} {task[:120]}\n")

    from elgringo.orchestrator import AIDevTeam
    from products.fred_api.coding_endpoints import (
        CodingAgentEngine,
        CodingTaskRequest,
        ProjectTools,
    )

    team = AIDevTeam(enable_memory=True)
    if not team.agents:
        print(f"{Colors.RED}No agents available. Set API keys.{Colors.RESET}")
        return

    tools = ProjectTools(project_path)
    engine = CodingAgentEngine(team=team, tools=tools)

    allowed_agents = config.get("preferred_agents") if config.get("preferred_agents") else None

    # Step 1: Plan first unless --yes or --dry-run
    if not dry_run and not yes:
        print(f"  {Colors.DIM}Planning...{Colors.RESET}\n")
        plan_request = CodingTaskRequest(
            task=task,
            project_path=project_path,
            files_to_read=files or [],
            run_tests=False,
            test_command=test_cmd,
            auto_commit=False,
            dry_run=True,
            allowed_agents=allowed_agents,
        )

        try:
            plan_result = await engine.execute_task(plan_request)
        except Exception as e:
            print(f"\n{Colors.RED}  Planning failed: {e}{Colors.RESET}\n")
            return

        if plan_result.plan:
            print(f"  {Colors.BOLD}Plan:{Colors.RESET}")
            for step in plan_result.plan[:15]:
                print(f"    {Colors.DIM}{step}{Colors.RESET}")
            print()

        print(f"  {Colors.DIM}Agents: {', '.join(plan_result.agents_used)}{Colors.RESET}")
        print()

        try:
            if sys.stdin.isatty():
                confirm = input(f"  {Colors.BOLD}Apply these changes? [y/N]{Colors.RESET} ").strip().lower()
            else:
                tty = open("/dev/tty", "r")
                print(f"  {Colors.BOLD}Apply these changes? [y/N]{Colors.RESET} ", end="", flush=True)
                confirm = tty.readline().strip().lower()
                tty.close()
        except (EOFError, OSError):
            confirm = "y"

        if confirm != "y":
            print(f"\n  {Colors.DIM}Cancelled.{Colors.RESET}\n")
            return
        print()

    # Step 2: Execute
    engine = CodingAgentEngine(team=team, tools=tools)

    request = CodingTaskRequest(
        task=task,
        project_path=project_path,
        files_to_read=files or [],
        run_tests=not no_tests,
        test_command=test_cmd,
        auto_commit=auto_commit,
        dry_run=dry_run,
        allowed_agents=allowed_agents,
    )

    start = _time.time()
    if not dry_run:
        print(f"  {Colors.DIM}Applying changes...{Colors.RESET}\n")
    else:
        print(f"  {Colors.DIM}Planning...{Colors.RESET}\n")

    try:
        result = await engine.execute_task(request)
    except Exception as e:
        print(f"\n{Colors.RED}  FAILED: {e}{Colors.RESET}\n")
        import traceback
        traceback.print_exc()
        return

    elapsed = _time.time() - start

    # Step 3: Save backup for undo/diff
    if not dry_run and engine._backups and result.files_changed:
        save_backup(project_path, engine._backups, result.files_changed)

    # Display results
    print(f"\n{Colors.BOLD}{'─' * 60}{Colors.RESET}")

    status_color = Colors.GREEN if result.status == "success" else (
        Colors.YELLOW if result.status in ("partial", "dry_run") else Colors.RED
    )
    print(f"{status_color}  {result.status.upper()}{Colors.RESET} in {elapsed:.1f}s ({result.iterations} iteration{'s' if result.iterations != 1 else ''})")
    print(f"  Agents: {', '.join(result.agents_used)}")
    print(f"{Colors.BOLD}{'─' * 60}{Colors.RESET}\n")

    if result.plan:
        print(f"  {Colors.BOLD}Plan:{Colors.RESET}")
        for step in result.plan:
            print(f"    • {step}")
        print()

    if result.files_changed:
        print(f"  {Colors.BOLD}Files changed ({len(result.files_changed)}):{Colors.RESET}")
        for fc in result.files_changed:
            icon = {"created": "+", "modified": "~", "deleted": "-"}.get(fc.action, "?")
            color = {"created": Colors.GREEN, "modified": Colors.YELLOW, "deleted": Colors.RED}.get(fc.action, Colors.RESET)
            print(f"    {color}{icon} {fc.path}{Colors.RESET} ({fc.lines_changed} lines)")
        print()

    if result.test_results:
        tr = result.test_results
        test_icon = "✅" if tr.passed else "❌"
        print(f"  {Colors.BOLD}Tests:{Colors.RESET} {test_icon} {tr.command} ({tr.duration_seconds:.1f}s)")
        if not tr.passed:
            lines = tr.output.strip().split("\n")
            for line in lines[-10:]:
                print(f"    {Colors.DIM}{line}{Colors.RESET}")
        print()

    if result.git_commit:
        print(f"  {Colors.GREEN}Committed: {result.git_commit}{Colors.RESET}\n")

    if result.errors:
        print(f"  {Colors.RED}Errors:{Colors.RESET}")
        for err in result.errors:
            print(f"    • {err}")
        print()

    print(f"  {Colors.BOLD}Summary:{Colors.RESET} {result.summary}\n")

    if not dry_run and result.files_changed:
        print(f"  {Colors.DIM}Tip: 'elgringo undo' to revert, 'elgringo diff' to review changes{Colors.RESET}\n")

    return result
