"""Security utilities for input validation and sanitization."""

from __future__ import annotations

import logging
import os
import re
import sys
from pathlib import Path

log = logging.getLogger(__name__)

SECRET_PATTERNS: dict[str, re.Pattern[str]] = {
    "OpenAI API Key": re.compile(r"sk-[a-zA-Z0-9]{20,}"),
    "Anthropic API Key": re.compile(r"sk-ant-[a-zA-Z0-9]{20,}"),
    "Google API Key": re.compile(r"AIza[a-zA-Z0-9_-]{33}"),
    "DeepSeek API Key": re.compile(r"sk-[a-f0-9]{32,}"),
    "Generic Secret": re.compile(
        r"(?:secret|password|token|api_key)\s*[=:]\s*['\"][^'\"]{10,}['\"]",
        re.IGNORECASE,
    ),
}

SCANNABLE_EXTENSIONS: frozenset[str] = frozenset(
    {".py", ".yaml", ".yml", ".json", ".md", ".txt", ".env", ".cfg", ".ini", ".toml", ".sh"}
)

_MAX_FILE_SIZE = 1_048_576  # 1 MB


def sanitize_filename(filename: str, max_length: int = 255) -> str:
    """Sanitize filename to prevent directory traversal and injection attacks."""
    filename = filename.replace("\x00", "")
    filename = filename.replace("/", "_").replace("\\", "_")
    filename = filename.replace("..", "_")
    dangerous_chars = r'[<>:"|?*;`$&(){}[\]!#%^]'
    filename = re.sub(dangerous_chars, "_", filename)
    filename = "".join(c if ord(c) >= 32 else "_" for c in filename)
    filename = filename.strip()
    if not filename or filename in (".", "..", "_"):
        filename = "untitled"
    if len(filename) > max_length:
        name, ext = os.path.splitext(filename)
        max_name_len = max_length - len(ext)
        filename = name[:max_name_len] + ext
    return filename


def validate_file_path(
    filepath: str,
    allowed_dirs: list[str] | None = None,
    must_exist: bool = False,
) -> bool:
    """Validate file path to prevent directory traversal attacks."""
    try:
        resolved = Path(filepath).resolve()
        if "\x00" in str(filepath):
            return False
        if must_exist and not resolved.exists():
            return False
        if allowed_dirs:
            resolved_str = str(resolved)
            allowed = False
            for allowed_dir in allowed_dirs:
                allowed_path = str(Path(allowed_dir).resolve())
                if resolved_str.startswith(allowed_path + os.sep) or resolved_str == allowed_path:
                    allowed = True
                    break
            if not allowed:
                return False
        return True
    except (ValueError, OSError, RuntimeError):
        return False


def sanitize_template_name(name: str) -> str:
    """Sanitize template name for safe storage and retrieval."""
    name = name.replace("\x00", "")
    name = re.sub(r"<[^>]*>", "", name)
    name = name.replace("/", "_").replace("\\", "_")
    name = name.replace("..", "_")
    name = re.sub(r'[<>:"|?*/\\;`$&(){}[\]!#%^]', "_", name)
    name = re.sub(r"_{2,}", "_", name)
    name = name.strip().strip("_")
    if not name:
        name = "untitled"
    if len(name) > 100:
        name = name[:100]
    return name


def validate_url(url: str, allowed_schemes: list[str] | None = None) -> bool:
    """Validate URL to ensure it uses allowed schemes (e.g., https only)."""
    if allowed_schemes is None:
        allowed_schemes = ["https"]
    if "\x00" in url or any(c in url for c in ("\n", "\r")):
        return False
    from urllib.parse import urlparse

    try:
        parsed = urlparse(url.strip())
    except Exception:
        return False
    if parsed.scheme.lower() not in allowed_schemes:
        return False
    if not parsed.netloc:
        return False
    return True


def sanitize_config_value(value: str, max_length: int = 1000) -> str:
    """Sanitize configuration value to prevent injection attacks."""
    value = value.replace("\x00", "")
    value = "".join(c if ord(c) >= 32 or c in "\n\t" else "" for c in value)
    if len(value) > max_length:
        value = value[:max_length]
    return value


def sanitize_api_key(api_key: str) -> str:
    """Sanitize API key by removing whitespace and validating format."""
    api_key = api_key.strip()
    if "\x00" in api_key:
        return ""
    if not re.match(r"^[a-zA-Z0-9._-]+$", api_key):
        return ""
    if len(api_key) < 10:
        return ""
    return api_key


def mask_sensitive_data(text: str) -> str:
    """Mask sensitive data in text for safe logging."""
    patterns = [
        r"(sk-[a-zA-Z0-9]{8,})",
        r"(sk-ant-[a-zA-Z0-9]{8,})",
        r"(AIza[a-zA-Z0-9_-]{33})",
    ]
    masked = text
    for pattern in patterns:
        masked = re.sub(
            pattern,
            lambda m: m.group(0)[:5] + "***" if len(m.group(0)) > 5 else "***",
            masked,
        )
    return masked


def _mask_match(value: str) -> str:
    """Mask a matched secret value, keeping a short prefix visible."""
    if len(value) > 8:
        return value[:5] + "***"
    return "***"


def _collect_files(paths: list[str | Path]) -> list[Path]:
    """Resolve paths into a flat list of scannable files."""
    files: list[Path] = []
    for p in paths:
        path = Path(p)
        if path.is_dir():
            for child in path.rglob("*"):
                if child.is_file() and child.suffix in SCANNABLE_EXTENSIONS:
                    files.append(child)
        elif path.is_file() and path.suffix in SCANNABLE_EXTENSIONS:
            files.append(path)
    return files


def scan_for_secrets(
    paths: list[str | Path],
    *,
    extra_patterns: list[str] | None = None,
) -> list[dict[str, str]]:
    """Scan files for potential secrets and sensitive data."""
    patterns: dict[str, re.Pattern[str]] = dict(SECRET_PATTERNS)
    if extra_patterns:
        for i, pat in enumerate(extra_patterns):
            patterns[f"Custom Pattern {i + 1}"] = re.compile(pat)

    files = _collect_files(paths)
    findings: list[dict[str, str]] = []

    for filepath in files:
        try:
            if filepath.stat().st_size > _MAX_FILE_SIZE:
                continue
        except OSError:
            continue
        try:
            lines = filepath.read_text(encoding="utf-8").splitlines()
        except (UnicodeDecodeError, OSError):
            continue
        for line_num, line in enumerate(lines, start=1):
            for pattern_name, regex in patterns.items():
                for match in regex.finditer(line):
                    findings.append(
                        {
                            "file": str(filepath),
                            "line_number": str(line_num),
                            "pattern_name": pattern_name,
                            "masked_match": _mask_match(match.group(0)),
                        }
                    )

    findings.sort(key=lambda f: (f["file"], int(f["line_number"])))
    return findings


def store_api_key_secure(service: str, account: str, key: str) -> bool:
    """Store API key in platform-native secure storage."""
    try:
        import keyring

        if sys.platform == "win32":
            import keyring.backends.Windows

            keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring())
        keyring.set_password(service, account, key)
        return True
    except Exception as exc:
        log.warning("Failed to store key in secure storage: %s", exc)
        return False
