"""Config parser — load nestdx.yaml, validate, return typed NestConfig."""

from __future__ import annotations

import logging
import re
import subprocess
from pathlib import Path

import yaml

from nestdx.config.schema import (
    EnvironmentConfig,
    FilesystemPolicy,
    NestConfig,
    NetworkPolicy,
    Policies,
    ResourcePolicy,
    SecretsPolicy,
    ToolConfig,
    TrackingConfig,
    parse_duration,
)

logger = logging.getLogger(__name__)

CONFIG_FILENAME = "nestdx.yaml"

KNOWN_TOP_LEVEL_KEYS = {"name", "version", "environment", "tools", "policies", "tracking"}
KNOWN_TOOL_NAMES = {"claude-code", "codex", "cursor", "hdx-agent"}
KNOWN_POLICY_KEYS = {"secrets", "filesystem", "network", "resources"}
KNOWN_TRACKING_KEYS = {"log_file_changes", "log_commands", "track_tokens", "track_cost", "session_dir", "export_format"}


class ConfigNotFoundError(Exception):
    """No nestdx.yaml found."""


class ConfigValidationError(Exception):
    """Invalid configuration."""

    def __init__(self, errors: list[str]):
        self.errors = errors
        super().__init__("Config validation failed:\n" + "\n".join(f"  - {e}" for e in errors))


def find_config(start: Path | None = None) -> Path:
    """Search cwd upward for nestdx.yaml, stopping at git root or filesystem root."""
    current = (start or Path.cwd()).resolve()

    while True:
        candidate = current / CONFIG_FILENAME
        if candidate.is_file():
            return candidate

        # Stop at git root
        if (current / ".git").exists():
            break

        parent = current.parent
        if parent == current:
            break
        current = parent

    raise ConfigNotFoundError(
        f"No {CONFIG_FILENAME} found. Run `nestdx init` to create one."
    )


def _git_root(start: Path) -> Path | None:
    """Find the git repo root, or None."""
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=start,
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return Path(result.stdout.strip())
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass
    return None


def _parse_tool_config(raw: dict) -> ToolConfig:
    return ToolConfig(
        enabled=raw.get("enabled", True),
        reason=raw.get("reason"),
        config=raw.get("config", {}),
        agent_path=raw.get("agent_path"),
    )


def _parse_secrets_policy(raw: dict) -> SecretsPolicy:
    defaults = SecretsPolicy()
    return SecretsPolicy(
        scan_agent_output=raw.get("scan_agent_output", defaults.scan_agent_output),
        scan_commits=raw.get("scan_commits", defaults.scan_commits),
        block_patterns=raw.get("block_patterns", defaults.block_patterns),
        custom_patterns_file=raw.get("custom_patterns_file"),
    )


def _parse_filesystem_policy(raw: dict) -> FilesystemPolicy:
    defaults = FilesystemPolicy()
    return FilesystemPolicy(
        read_only=raw.get("read_only", defaults.read_only),
        no_write=raw.get("no_write", defaults.no_write),
        allowed_write=raw.get("allowed_write", defaults.allowed_write),
    )


def _parse_network_policy(raw: dict) -> NetworkPolicy:
    return NetworkPolicy(
        allowed_domains=raw.get("allowed_domains", []),
        block_all_other_egress=raw.get("block_all_other_egress", False),
    )


def _parse_resource_policy(raw: dict) -> ResourcePolicy:
    return ResourcePolicy(
        max_cpu=raw.get("max_cpu"),
        max_memory=raw.get("max_memory"),
        max_session_duration=raw.get("max_session_duration"),
    )


def _parse_policies(raw: dict) -> Policies:
    return Policies(
        secrets=_parse_secrets_policy(raw.get("secrets", {})),
        filesystem=_parse_filesystem_policy(raw.get("filesystem", {})),
        network=_parse_network_policy(raw.get("network", {})),
        resources=_parse_resource_policy(raw.get("resources", {})),
    )


def _parse_environment(raw: dict) -> EnvironmentConfig:
    defaults = EnvironmentConfig()
    return EnvironmentConfig(
        base=raw.get("base", defaults.base),
        setup=raw.get("setup", defaults.setup),
        test_command=raw.get("test_command"),
        lint_command=raw.get("lint_command"),
        env_file=raw.get("env_file"),
        dockerfile=raw.get("dockerfile"),
        volumes=raw.get("volumes", defaults.volumes),
        cache_dirs=raw.get("cache_dirs", defaults.cache_dirs),
    )


def _parse_tracking(raw: dict) -> TrackingConfig:
    defaults = TrackingConfig()
    return TrackingConfig(
        log_file_changes=raw.get("log_file_changes", defaults.log_file_changes),
        log_commands=raw.get("log_commands", defaults.log_commands),
        track_tokens=raw.get("track_tokens", defaults.track_tokens),
        track_cost=raw.get("track_cost", defaults.track_cost),
        session_dir=raw.get("session_dir", defaults.session_dir),
        export_format=raw.get("export_format", defaults.export_format),
    )


def _validate(raw: dict) -> list[str]:
    """Validate raw YAML dict. Returns list of error strings."""
    errors: list[str] = []
    warnings: list[str] = []

    if not isinstance(raw, dict):
        return ["Config must be a YAML mapping."]

    # Required fields
    if "name" not in raw:
        errors.append("Missing required field: 'name'")
    elif not isinstance(raw["name"], str) or not raw["name"].strip():
        errors.append("'name' must be a non-empty string.")

    if "version" in raw and not isinstance(raw["version"], str):
        errors.append("'version' must be a string (e.g. '0.1.0').")

    # Unknown top-level keys — warn, don't fail
    for key in raw:
        if key not in KNOWN_TOP_LEVEL_KEYS:
            warnings.append(f"Unknown top-level key '{key}' — will be ignored.")

    # Validate tools section
    tools = raw.get("tools", {})
    if tools and not isinstance(tools, dict):
        errors.append("'tools' must be a mapping (e.g. tools:\\n  claude-code:\\n    enabled: true).")
    elif isinstance(tools, dict):
        for tool_name, tool_raw in tools.items():
            if not isinstance(tool_raw, dict):
                errors.append(f"tools.{tool_name} must be a mapping.")

    # Validate policies section
    policies = raw.get("policies", {})
    if policies and not isinstance(policies, dict):
        errors.append("'policies' must be a mapping.")
    elif isinstance(policies, dict):
        for key in policies:
            if key not in KNOWN_POLICY_KEYS:
                warnings.append(f"Unknown policy key 'policies.{key}' — will be ignored.")

        secrets = policies.get("secrets", {})
        if isinstance(secrets, dict):
            for pattern in secrets.get("block_patterns", []):
                try:
                    re.compile(pattern)
                except re.error as e:
                    errors.append(f"Invalid regex in secrets.block_patterns: '{pattern}' — {e}")

        # Validate max_session_duration format
        resources = policies.get("resources", {})
        if isinstance(resources, dict):
            duration_str = resources.get("max_session_duration")
            if duration_str is not None:
                try:
                    parse_duration(str(duration_str))
                except ValueError as e:
                    errors.append(f"policies.resources.max_session_duration: {e}")

    # Validate tracking section
    tracking = raw.get("tracking", {})
    if tracking and not isinstance(tracking, dict):
        errors.append("'tracking' must be a mapping.")
    elif isinstance(tracking, dict):
        for key in tracking:
            if key not in KNOWN_TRACKING_KEYS:
                warnings.append(f"Unknown tracking key 'tracking.{key}' — will be ignored.")

    # Emit warnings via logger (don't fail)
    for w in warnings:
        logger.warning(w)

    return errors


def load_config(path: Path | None = None) -> NestConfig:
    """Load and validate nestdx.yaml.

    Args:
        path: Explicit path to config file. If None, searches cwd upward.

    Returns:
        Validated NestConfig.

    Raises:
        ConfigNotFoundError: No nestdx.yaml found.
        ConfigValidationError: Invalid config with details.
    """
    config_path = path if path else find_config()

    if not config_path.is_file():
        raise ConfigNotFoundError(f"Config file not found: {config_path}")

    try:
        raw = yaml.safe_load(config_path.read_text())
    except yaml.YAMLError as e:
        # PyYAML includes line/column in MarkedYAMLError subclasses
        if hasattr(e, "problem_mark") and e.problem_mark is not None:
            mark = e.problem_mark
            msg = f"YAML parse error at line {mark.line + 1}, column {mark.column + 1}: {e.problem}"
        else:
            msg = f"YAML parse error: {e}"
        raise ConfigValidationError([msg])

    if raw is None:
        raise ConfigValidationError(["Config file is empty."])

    errors = _validate(raw)
    if errors:
        raise ConfigValidationError(errors)

    # Parse tools
    tools: dict[str, ToolConfig] = {}
    for tool_name, tool_raw in raw.get("tools", {}).items():
        tools[tool_name] = _parse_tool_config(tool_raw)

    # Parse optional environment block
    env_raw = raw.get("environment")
    environment = _parse_environment(env_raw) if isinstance(env_raw, dict) else None

    return NestConfig(
        name=raw["name"],
        version=raw.get("version", "0.1.0"),
        tools=tools,
        policies=_parse_policies(raw.get("policies", {})),
        tracking=_parse_tracking(raw.get("tracking", {})),
        environment=environment,
    )
