"""Type checking for fix validation.

Uses Ty (Astral) for fast type checking (10-60x faster than mypy),
with mypy fallback if Ty is not available.
"""

from __future__ import annotations

import json
import os
import re
import subprocess
import tempfile
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple

if TYPE_CHECKING:
    from autodebug.validation.fix_validator import ValidationError


@dataclass
class TypeCheckError:
    """A type checking error."""

    message: str
    line: Optional[int] = None
    column: Optional[int] = None
    code: Optional[str] = None  # Error code like "type-arg"


class TyChecker:
    """Type checker using Ty (Astral) - 10-60x faster than mypy.

    Ty is a Rust-based type checker from Astral (makers of Ruff).
    It provides significantly faster type checking than mypy while
    maintaining compatibility with Python type hints.
    """

    def __init__(self, strict: bool = False):
        """Initialize the Ty type checker.

        Args:
            strict: Enable strict mode for more thorough checking
        """
        self.strict = strict
        self._ty_available: Optional[bool] = None

    @property
    def ty_available(self) -> bool:
        """Check if Ty is installed (cached)."""
        if self._ty_available is None:
            self._ty_available = self._check_ty_available()
        return self._ty_available

    def _check_ty_available(self) -> bool:
        """Check if Ty is installed."""
        try:
            result = subprocess.run(
                ["ty", "--version"],
                capture_output=True,
                check=True,
                timeout=5
            )
            return True
        except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
            return False

    def check(self, code: str) -> Tuple[bool, List[ValidationError]]:
        """Run Ty type checker on code.

        Args:
            code: Python source code to check

        Returns:
            Tuple of (types_ok, list of errors)
        """
        # Import here to avoid circular import
        from autodebug.validation.fix_validator import ValidationError

        if not self.ty_available:
            # Fallback to mypy
            return MypyChecker().check(code)

        with tempfile.NamedTemporaryFile(
            suffix=".py", delete=False, mode="w", encoding="utf-8"
        ) as f:
            f.write(code)
            f.flush()
            temp_path = f.name

        try:
            args = ["ty", "check", temp_path]

            # Try JSON output first (may not be supported in all versions)
            try:
                result = subprocess.run(
                    args + ["--output-format", "json"],
                    capture_output=True,
                    text=True,
                    timeout=10,  # Ty is fast, 10s is plenty
                )
                errors = self._parse_json_output(result.stdout, result.stderr)
            except (subprocess.TimeoutExpired, Exception):
                # Fall back to plain output
                result = subprocess.run(
                    args,
                    capture_output=True,
                    text=True,
                    timeout=10,
                )
                errors = self._parse_plain_output(result.stdout + result.stderr)

            # Convert to ValidationError
            validation_errors = [
                ValidationError(
                    stage="type",
                    message=e.message,
                    line=e.line,
                    column=e.column,
                )
                for e in errors
            ]

            return len(errors) == 0, validation_errors

        except subprocess.TimeoutExpired:
            # Assume OK if timeout (shouldn't happen with Ty)
            return True, []
        finally:
            try:
                os.unlink(temp_path)
            except OSError:
                pass

    def _parse_json_output(self, stdout: str, stderr: str) -> List[TypeCheckError]:
        """Parse Ty JSON output."""
        output = stdout or stderr
        if not output.strip():
            return []

        errors = []
        try:
            data = json.loads(output)
            for item in data.get("diagnostics", []):
                errors.append(TypeCheckError(
                    message=item.get("message", ""),
                    line=item.get("range", {}).get("start", {}).get("line"),
                    column=item.get("range", {}).get("start", {}).get("character"),
                    code=item.get("code"),
                ))
        except json.JSONDecodeError:
            # Fall back to plain parsing
            return self._parse_plain_output(output)

        return errors

    def _parse_plain_output(self, output: str) -> List[TypeCheckError]:
        """Parse Ty plain text output."""
        if not output.strip():
            return []

        errors = []
        for line in output.strip().split("\n"):
            # Try to match error pattern: file.py:line:col: error: message
            match = re.match(r"[^:]+:(\d+):(\d+):\s*(error|warning):\s*(.+)", line)
            if match:
                errors.append(TypeCheckError(
                    message=match.group(4),
                    line=int(match.group(1)),
                    column=int(match.group(2)),
                ))
            elif ": error:" in line:
                # Simpler format: error: message
                errors.append(TypeCheckError(
                    message=line.split(": error:")[-1].strip(),
                ))

        return errors


class MypyChecker:
    """Fallback type checker using mypy.

    Used when Ty is not available. Mypy is slower but well-established
    and widely supported.
    """

    def __init__(self, strict: bool = False):
        """Initialize the mypy checker.

        Args:
            strict: Enable strict mode
        """
        self.strict = strict
        self._mypy_available: Optional[bool] = None

    @property
    def mypy_available(self) -> bool:
        """Check if mypy is available (cached)."""
        if self._mypy_available is None:
            try:
                from mypy import api
                self._mypy_available = True
            except ImportError:
                self._mypy_available = False
        return self._mypy_available

    def check(self, code: str) -> Tuple[bool, List[ValidationError]]:
        """Run mypy on code.

        Args:
            code: Python source code to check

        Returns:
            Tuple of (types_ok, list of errors)
        """
        # Import here to avoid circular import
        from autodebug.validation.fix_validator import ValidationError

        if not self.mypy_available:
            # Skip type checking if mypy not installed
            return True, []

        from mypy import api

        with tempfile.NamedTemporaryFile(
            suffix=".py", delete=False, mode="w", encoding="utf-8"
        ) as f:
            f.write(code)
            f.flush()
            temp_path = f.name
        config_path = None

        try:
            # Use a minimal config to avoid project-level mypy settings
            # (e.g. ignore_errors) affecting single-file checks.
            with tempfile.NamedTemporaryFile(
                suffix=".ini", delete=False, mode="w", encoding="utf-8"
            ) as cfg:
                cfg.write("[mypy]\n")
                cfg.write("ignore_missing_imports = True\n")
                cfg.write("warn_return_any = False\n")
                cfg.write("disallow_untyped_defs = False\n")
                cfg.flush()
                config_path = cfg.name

            args = [
                temp_path,
                "--no-error-summary",
                "--show-error-codes",
                "--no-color-output",
                "--config-file",
                config_path,
            ]
            if self.strict:
                args.append("--strict")

            stdout, stderr, exit_code = api.run(args)

            errors = self._parse_output(stdout)

            validation_errors = [
                ValidationError(
                    stage="type",
                    message=e.message,
                    line=e.line,
                    column=e.column,
                )
                for e in errors
            ]

            return exit_code == 0, validation_errors

        finally:
            try:
                os.unlink(temp_path)
            except OSError:
                pass
            if config_path:
                try:
                    os.unlink(config_path)
                except OSError:
                    pass

    def _parse_output(self, output: str) -> List[TypeCheckError]:
        """Parse mypy output."""
        if not output.strip():
            return []

        errors = []
        for line in output.strip().split("\n"):
            # Match: file.py:line: error: message [code]
            match = re.match(
                r"[^:]+:(\d+)(?::(\d+))?: (error|warning): (.+?)(?:\s+\[(.+)\])?$",
                line
            )
            if match:
                errors.append(TypeCheckError(
                    message=match.group(4),
                    line=int(match.group(1)),
                    column=int(match.group(2)) if match.group(2) else None,
                    code=match.group(5),
                ))

        return errors


class RuffChecker:
    """Fast linter using Ruff for quick syntax/style checks.

    While not a full type checker, Ruff catches many common errors
    very quickly and can be used as a first pass before type checking.
    """

    def __init__(self):
        self._ruff_available: Optional[bool] = None

    @property
    def ruff_available(self) -> bool:
        """Check if Ruff is available (cached)."""
        if self._ruff_available is None:
            try:
                result = subprocess.run(
                    ["ruff", "--version"],
                    capture_output=True,
                    check=True,
                    timeout=5
                )
                self._ruff_available = True
            except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
                self._ruff_available = False
        return self._ruff_available

    def check(self, code: str) -> Tuple[bool, List[ValidationError]]:
        """Run Ruff linter on code.

        Args:
            code: Python source code to check

        Returns:
            Tuple of (ok, list of errors)
        """
        from autodebug.validation.fix_validator import ValidationError

        if not self.ruff_available:
            return True, []

        with tempfile.NamedTemporaryFile(
            suffix=".py", delete=False, mode="w", encoding="utf-8"
        ) as f:
            f.write(code)
            f.flush()
            temp_path = f.name

        try:
            result = subprocess.run(
                ["ruff", "check", "--output-format=json", temp_path],
                capture_output=True,
                text=True,
                timeout=5,
            )

            errors = []
            if result.stdout.strip():
                try:
                    data = json.loads(result.stdout)
                    for item in data:
                        errors.append(ValidationError(
                            stage="lint",
                            message=f"[{item.get('code', '?')}] {item.get('message', '')}",
                            line=item.get("location", {}).get("row"),
                            column=item.get("location", {}).get("column"),
                        ))
                except json.JSONDecodeError:
                    pass

            return len(errors) == 0, errors

        except subprocess.TimeoutExpired:
            return True, []
        finally:
            try:
                os.unlink(temp_path)
            except OSError:
                pass
