"""Runner that orchestrates all checkers."""

from __future__ import annotations

import fnmatch
from pathlib import Path
from typing import List, Optional, Tuple, Type

from stablestack.checkers import FREE_CHECKERS, get_all_checkers
from stablestack.checkers.base import BaseChecker
from stablestack.config import Config
from stablestack.licensing import get_cached_license
from stablestack.models import Finding, Severity, Tier


def run_checks(
    paths: List[Path],
    config: Config | None = None,
    checker_classes: List[Type[BaseChecker]] | None = None,
    use_cache: bool = False,
) -> Tuple[List[Finding], bool, int]:
    """
    Run all enabled checkers on the given paths.

    Args:
        paths: List of files or directories to check
        config: Optional configuration object
        checker_classes: Optional list of checker classes to use (overrides config)
        use_cache: Whether to use file caching for faster repeated runs

    Returns:
        Tuple of (findings list, is_free_tier, total_checkers_available)
    """
    config = config or Config()
    all_findings: List[Finding] = []

    # Determine which checkers to use
    is_free_tier = False
    if checker_classes is not None:
        checkers = [cls() for cls in checker_classes]
    else:
        checkers, is_free_tier = _get_enabled_checkers(config)

    # Count total available checkers (for "upgrade" messaging)
    total_checkers = len(get_all_checkers())

    # Separate file-level and project-level checkers
    file_checkers = [c for c in checkers if not getattr(c, 'is_project_checker', False)]
    project_checkers = [c for c in checkers if getattr(c, 'is_project_checker', False)]

    # Set up caching
    cache = None
    if use_cache and paths:
        from stablestack.cache import FileCache
        # Use first path's parent as project root
        project_root = paths[0] if paths[0].is_dir() else paths[0].parent
        cache = FileCache(project_root.resolve(), enabled=True)

    # Run project-level checks on directories
    project_roots_checked = set()  # type: set[Path]
    for path in paths:
        project_root = path if path.is_dir() else path.parent
        project_root = project_root.resolve()

        if project_root not in project_roots_checked:
            project_roots_checked.add(project_root)
            for checker in project_checkers:
                _apply_config_overrides(checker, config)
                try:
                    findings = checker.check_project(project_root)
                    if findings:
                        all_findings.extend(findings)
                except Exception:
                    pass  # Skip broken checkers gracefully

    # Expand directories to files
    files = _expand_paths(paths, config)

    # Run file-level checks on each file
    for file_path in files:
        # Check cache first
        if cache and cache.is_cached(file_path):
            cached_findings = cache.get_findings(file_path)
            if cached_findings is not None:
                # Restore Finding objects from cached data
                for cf in cached_findings:
                    all_findings.append(Finding(
                        file=str(file_path),
                        line=cf["line"],
                        column=cf["column"],
                        code=cf["code"],
                        rule_id=cf["rule_id"],
                        rule_name=cf["rule_name"],
                        message=cf["message"],
                        severity=Severity(cf["severity"]),
                        fix_hint=cf["fix_hint"],
                        why=cf["why"],
                    ))
                continue

        content = _read_file(file_path)
        if content is None:
            continue

        file_findings: List[Finding] = []
        for checker in file_checkers:
            if not checker.supports_file(file_path):
                continue
            if checker.should_skip_file(file_path):
                continue

            # Apply severity override from config
            _apply_config_overrides(checker, config)

            try:
                findings = checker.check_file(file_path, content)
                if not findings:
                    continue
                # Filter out suppressed findings
                findings = _filter_suppressed(findings, content)
                file_findings.extend(findings)
            except Exception:
                pass  # Skip broken checkers gracefully

        all_findings.extend(file_findings)

        # Cache the results
        if cache:
            cache.set_findings(file_path, file_findings)

    # Save cache
    if cache:
        cache.save()

    return all_findings, is_free_tier, total_checkers


def _filter_suppressed(findings: List[Finding], content: str) -> List[Finding]:
    """Filter out findings that have suppression comments."""
    lines = content.splitlines()
    filtered = []

    for finding in findings:
        line_idx = finding.line - 1
        if line_idx < 0 or line_idx >= len(lines):
            filtered.append(finding)
            continue

        # Check current line for inline suppression
        current_line = lines[line_idx]
        if "stablestack: ignore" in current_line or "noqa" in current_line:
            continue

        # Check previous line for suppression comment (# for Python, // for JS/TS)
        if line_idx > 0:
            prev_line = lines[line_idx - 1].strip()
            if (prev_line.startswith("#") or prev_line.startswith("//")) and (
                "stablestack: ignore" in prev_line or "noqa" in prev_line
            ):
                continue

        filtered.append(finding)

    return filtered


def _get_enabled_checkers(config: Config) -> Tuple[List[BaseChecker], bool]:
    """
    Get list of enabled checker instances based on config and license.

    When unlicensed (free tier), only FREE_CHECKERS are used.
    When licensed, all checkers (free + paid compiled) are available.

    Returns:
        Tuple of (checkers list, is_free_tier boolean)
    """
    checkers = []

    # Check license status
    is_licensed, _, _ = get_cached_license()
    is_free_tier = not is_licensed

    # Pick the source list based on license status
    available = FREE_CHECKERS if is_free_tier else get_all_checkers()

    for checker_cls in available:
        rule_id = checker_cls.rule_id

        # Skip if explicitly disabled
        if rule_id in config.disable:
            continue

        # If enable list is specified, only include those
        if config.enable and rule_id not in config.enable:
            continue

        # Filter by exact tier if set (--tier flag: run ONLY this tier)
        if config.exact_tier is not None:
            checker_tier = getattr(checker_cls, 'tier', Tier.RECOMMENDED)
            if checker_tier != config.exact_tier:
                continue

        # Filter by max tier if set (--up-to flag: run up to and including this tier)
        if config.max_tier is not None:
            checker_tier = getattr(checker_cls, 'tier', Tier.RECOMMENDED)
            if checker_tier.value > config.max_tier.value:
                continue

        checkers.append(checker_cls())

    return checkers, is_free_tier


def _apply_config_overrides(checker: BaseChecker, config: Config) -> None:
    """Apply any config overrides to a checker instance."""
    rule_config = config.rules.get(checker.rule_id, {})

    # Override severity if specified
    if "severity" in rule_config:
        from stablestack.models import Severity
        try:
            checker.severity = Severity(rule_config["severity"])
        except ValueError:
            pass  # Invalid severity, keep default


def _expand_paths(paths: List[Path], config: Config) -> List[Path]:
    """Expand directories to individual files, applying include/exclude patterns."""
    files: List[Path] = []

    for path in paths:
        if path.is_file():
            if _should_include_file(path, config):
                files.append(path)
        elif path.is_dir():
            # Walk directory
            for file_path in path.rglob("*"):
                if file_path.is_file() and _should_include_file(file_path, config):
                    files.append(file_path)

    return sorted(set(files))


def _should_include_file(path: Path, config: Config) -> bool:
    """Check if a file should be included based on config patterns."""

    # Check exclude patterns first
    for pattern in config.exclude:
        if _glob_match(path, pattern):
            return False

    # If include patterns are specified, file must match at least one
    if config.include:
        for pattern in config.include:
            if _glob_match(path, pattern):
                return True
        return False

    # Default: include common source files
    return path.suffix in {".py", ".js", ".ts", ".jsx", ".tsx", ".vue"}


def _glob_match(path: Path, pattern: str) -> bool:
    """Match a path against a glob pattern with proper ** support."""
    path_str = str(path)

    # Simple fnmatch first (handles patterns without **)
    if fnmatch.fnmatch(path_str, pattern):
        return True

    # Handle ** patterns (match zero or more directories)
    if "**" in pattern:
        import re
        # Build regex piece by piece to avoid replacement conflicts
        parts = []
        i = 0
        while i < len(pattern):
            if pattern[i:i+3] == "**/":
                parts.append("(.*/)?")  # zero or more directories
                i += 3
            elif pattern[i:i+2] == "**":
                parts.append(".*")  # match anything
                i += 2
            elif pattern[i] == "*":
                parts.append("[^/]*")  # match within single directory
                i += 1
            elif pattern[i] == "?":
                parts.append("[^/]")  # match single char (not /)
                i += 1
            elif pattern[i] in ".[](){}+^$|":
                parts.append("\\" + pattern[i])  # escape regex special chars
                i += 1
            else:
                parts.append(pattern[i])
                i += 1

        regex_pattern = "^" + "".join(parts) + "$"
        if re.match(regex_pattern, path_str):
            return True

    return False


def _read_file(path: Path) -> str | None:
    """Read file content, returning None if it can't be read."""
    try:
        return path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        return None
