"""
Validation system for settings and files.
"""

import json
import re
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

try:
    from rich.console import Console

    RICH_AVAILABLE = True
except ImportError:
    RICH_AVAILABLE = False

from persuade.constants import OPTIONAL_DEPENDENCIES, REQUIRED_DEPENDENCIES
from persuade.file_ops import fix_executable_permissions, validate_executable_permissions
from persuade.logging import get_logger

logger = get_logger()

# Timeout for claude doctor subprocess (seconds)
CLAUDE_DOCTOR_TIMEOUT_SECONDS = 30

# ANSI escape sequence patterns for terminal output parsing
# CSI sequences: \x1b[...letter (colors, styles, cursor positioning)
_CSI_PATTERN = r"\x1b\[[0-9;]*[a-zA-Z]"
# OSC sequences: \x1b]...\x07 (window titles, hyperlinks)
_OSC_PATTERN = r"\x1b\][^\x07]*\x07"
# Private CSI: \x1b[?...letter (terminal modes like cursor visibility)
_PRIVATE_CSI_PATTERN = r"\x1b\[\?[0-9;]*[a-zA-Z]"
# Combined pattern for stripping all ANSI codes
_ANSI_PATTERN = re.compile(f"{_CSI_PATTERN}|{_OSC_PATTERN}|{_PRIVATE_CSI_PATTERN}")


@dataclass
class ValidationResult:
    """Result of validation check."""

    valid: bool
    issues: list[str]
    fixable: list[str]
    details: list[str] = field(default_factory=list)  # Info to show even when valid


@dataclass
class DoctorReport:
    """Report from doctor command."""

    total_checks: int = 0
    passed: int = 0
    failed: int = 0
    fixed: int = 0
    all_passed: bool = False


class Validator:
    """Base validator class."""

    def validate(self, path: Path) -> ValidationResult:
        """Validate and return result."""
        raise NotImplementedError

    def fix(self, path: Path) -> list[str]:
        """Auto-fix issues if possible."""
        return []


class JSONSyntaxValidator(Validator):
    """Validates JSON syntax in settings.json."""

    def validate(self, path: Path) -> ValidationResult:
        settings_path = path / ".claude" / "settings.json"

        if not settings_path.exists():
            return ValidationResult(valid=False, issues=["settings.json not found"], fixable=[])

        try:
            with open(settings_path) as f:
                json.load(f)
            return ValidationResult(valid=True, issues=[], fixable=[])
        except json.JSONDecodeError as e:
            return ValidationResult(
                valid=False,
                issues=[f"JSON syntax error at line {e.lineno}: {e.msg}"],
                fixable=[],
            )


class PermissionPatternValidator(Validator):
    """Validates permission patterns for correct wildcard syntax."""

    def validate(self, path: Path) -> ValidationResult:
        settings_path = path / ".claude" / "settings.json"

        if not settings_path.exists():
            return ValidationResult(valid=True, issues=[], fixable=[])

        try:
            with open(settings_path) as f:
                settings = json.load(f)
        except json.JSONDecodeError:
            # JSON syntax validator will catch this
            return ValidationResult(valid=True, issues=[], fixable=[])

        issues: list[str] = []
        fixable: list[str] = []

        # Check for old wildcard syntax (* instead of :*)
        for perm in settings.get("permissions", {}).get("allow", []):
            # Match patterns like "Bash(git *)" but not "Bash(git :*)"
            if "Bash(" in perm and " *" in perm and " :*" not in perm:
                fixable.append(f"Old wildcard syntax: {perm}")

        return ValidationResult(valid=len(issues) == 0, issues=issues, fixable=fixable)

    def fix(self, path: Path) -> list[str]:
        """Fix wildcard syntax."""
        settings_path = path / ".claude" / "settings.json"

        if not settings_path.exists():
            return []

        with open(settings_path) as f:
            settings = json.load(f)

        fixed = []
        allow_list = settings.get("permissions", {}).get("allow", [])

        for i, perm in enumerate(allow_list):
            if "Bash(" in perm and " *" in perm and " :*" not in perm:
                # Replace " *" with " :*"
                new_perm = perm.replace(" *)", " :*)")
                allow_list[i] = new_perm
                fixed.append(f"{perm} → {new_perm}")

        # Save if we made changes
        if fixed:
            with open(settings_path, "w") as f:
                json.dump(settings, f, indent=2)
                f.write("\n")

        return fixed


class ExecutablePermissionsValidator(Validator):
    """Validates executable bits on scripts."""

    def validate(self, path: Path) -> ValidationResult:
        missing = validate_executable_permissions(path)

        return ValidationResult(
            valid=len(missing) == 0,
            issues=[],
            fixable=[f"Missing +x: {p}" for p in missing],
        )

    def fix(self, path: Path) -> list[str]:
        """Auto-fix executable permissions."""
        return fix_executable_permissions(path)


class DependencyValidator(Validator):
    """Validates required system dependencies."""

    def validate(self, path: Path | None = None) -> ValidationResult:
        issues = []
        warnings = []

        for dep in REQUIRED_DEPENDENCIES:
            if not shutil.which(dep):
                issues.append(f"Required dependency missing: {dep}")

        for dep in OPTIONAL_DEPENDENCIES:
            if not shutil.which(dep):
                warnings.append(f"Optional dependency missing: {dep}")

        return ValidationResult(
            valid=len(issues) == 0,
            issues=issues,
            fixable=[],  # Can't auto-install system deps
        )


def _strip_ansi_codes(text: str) -> str:
    """Strip ANSI escape codes from text.

    Handles three types of escape sequences (see module-level constants):
    - CSI sequences: colors, styles, cursor positioning
    - OSC sequences: window titles, hyperlinks
    - Private CSI: terminal modes like cursor visibility
    """
    return _ANSI_PATTERN.sub("", text)


def _parse_claude_doctor_output(output: str) -> list[str]:
    """Parse claude doctor output to extract diagnostic details.

    Extracts lines starting with "└" which contain the diagnostic info.
    """
    clean = _strip_ansi_codes(output)
    details = []
    for line in clean.splitlines():
        line = line.strip()
        if line.startswith("└"):
            details.append(line)
    return details


class ClaudeDoctorValidator(Validator):
    """Runs claude doctor if available.

    Uses 'expect' to handle interactive prompts. Claude doctor uses Ink which
    requires raw TTY mode, and 'expect' provides the proper PTY environment.
    """

    # Expect script to run claude doctor and handle interactive prompts.
    # Each command explained:
    #   spawn claude doctor      - Start the claude doctor subprocess in a PTY
    #   expect { ... }           - Wait for patterns in output:
    #     "Press*Enter*continue" - Match the "Press Enter to continue" prompt
    #     { send "\\r" }         - Send Enter key when prompt appears
    #     exp_continue           - Keep waiting for more prompts (loop)
    #     eof                    - Exit when process ends
    #   catch wait result        - Capture the exit status of the spawned process
    #   exit [lindex $result 3]  - Return the exit code (index 3 of wait result)
    EXPECT_SCRIPT = """
spawn claude doctor
expect {
    "Press*Enter*continue" { send "\\r"; exp_continue }
    eof
}
catch wait result
exit [lindex $result 3]
"""

    def validate(self, path: Path) -> ValidationResult:
        # Validate path before subprocess call (defensive programming)
        if not path.exists() or not path.is_dir():
            return ValidationResult(
                valid=False,
                issues=[f"Invalid path: {path}"],
                fixable=[],
            )

        if not shutil.which("claude"):
            # Claude not installed - not an error, just skip
            return ValidationResult(valid=True, issues=[], fixable=[])

        if not shutil.which("expect"):
            # Expect not installed - skip claude doctor check
            return ValidationResult(
                valid=True,
                issues=[],
                fixable=["Install 'expect' to enable claude doctor check"],
            )

        try:
            result = subprocess.run(
                ["expect", "-c", self.EXPECT_SCRIPT],
                cwd=path,
                capture_output=True,
                text=True,
                timeout=CLAUDE_DOCTOR_TIMEOUT_SECONDS,
            )

            # Parse diagnostic details from output
            details = _parse_claude_doctor_output(result.stdout)

            if result.returncode == 0:
                return ValidationResult(valid=True, issues=[], fixable=[], details=details)
            else:
                return ValidationResult(
                    valid=False,
                    issues=[f"claude doctor reported issues:\n{_strip_ansi_codes(result.stdout)}"],
                    fixable=[],
                    details=details,
                )
        except subprocess.TimeoutExpired:
            logger.debug("claude_doctor_timeout", timeout=CLAUDE_DOCTOR_TIMEOUT_SECONDS)
            return ValidationResult(valid=False, issues=["claude doctor timed out"], fixable=[])
        except (subprocess.SubprocessError, OSError) as e:
            logger.debug("claude_doctor_error", error=str(e))
            return ValidationResult(valid=False, issues=[f"claude doctor failed: {e}"], fixable=[])


def doctor_command(target_path: Path, fix: bool = False, check_only: bool = False) -> DoctorReport:
    """
    Comprehensive validation and optional auto-fix.

    Args:
        target_path: Path to target project
        fix: If True, auto-fix issues
        check_only: If True, only report, don't fix even if fix=True

    Returns:
        DoctorReport with statistics
    """
    validators = [
        ("Dependencies", DependencyValidator()),
        ("JSON Syntax", JSONSyntaxValidator()),
        ("Permission Patterns", PermissionPatternValidator()),
        ("Executable Permissions", ExecutablePermissionsValidator()),
        ("Claude Doctor", ClaudeDoctorValidator()),
    ]

    report = DoctorReport()
    report.total_checks = len(validators)

    if RICH_AVAILABLE:
        console = Console()
        console.print("\n[bold blue]Running diagnostics...[/bold blue]\n")
    else:
        print("\nRunning diagnostics...\n")

    for name, validator in validators:
        if RICH_AVAILABLE:
            with console.status(f"Checking {name}..."):
                result = validator.validate(target_path)
        else:
            print(f"Checking {name}...")
            result = validator.validate(target_path)

        # Update report
        if result.valid:
            report.passed += 1
            if RICH_AVAILABLE:
                console.print(f"[green]✓[/green] {name}")
            else:
                print(f"✓ {name}")
        else:
            report.failed += 1
            if RICH_AVAILABLE:
                console.print(f"[red]✗[/red] {name}")
            else:
                print(f"✗ {name}")

            for issue in result.issues:
                if RICH_AVAILABLE:
                    console.print(f"  [red]•[/red] {issue}")
                else:
                    print(f"  • {issue}")

        # Show details (info even when valid, e.g., claude doctor diagnostics)
        if result.details:
            for detail in result.details:
                if RICH_AVAILABLE:
                    console.print(f"  [dim]{detail}[/dim]")
                else:
                    print(f"  {detail}")

        # Show fixable issues
        if result.fixable:
            if RICH_AVAILABLE:
                console.print("  [yellow]⚠[/yellow] Fixable issues found:")
            else:
                print("  ⚠ Fixable issues found:")

            for fixable in result.fixable:
                if RICH_AVAILABLE:
                    console.print(f"    [yellow]•[/yellow] {fixable}")
                else:
                    print(f"    • {fixable}")

            # Auto-fix if requested
            if fix and not check_only and hasattr(validator, "fix"):
                fixed = validator.fix(target_path)
                report.fixed += len(fixed)

                for item in fixed:
                    if RICH_AVAILABLE:
                        console.print(f"    [green]✓[/green] Fixed: {item}")
                    else:
                        print(f"    ✓ Fixed: {item}")

    # Update all_passed
    report.all_passed = report.failed == 0

    # Summary
    if RICH_AVAILABLE:
        console.print("\n[bold]Summary:[/bold]")
        console.print(f"  Total checks: {report.total_checks}")
        console.print(f"  [green]Passed: {report.passed}[/green]")
        console.print(f"  [red]Failed: {report.failed}[/red]")
        if report.fixed > 0:
            console.print(f"  [yellow]Fixed: {report.fixed}[/yellow]")
    else:
        print("\nSummary:")
        print(f"  Total checks: {report.total_checks}")
        print(f"  Passed: {report.passed}")
        print(f"  Failed: {report.failed}")
        if report.fixed > 0:
            print(f"  Fixed: {report.fixed}")

    return report
