"""
Configuration I/O for Lattice.

Shell layer - performs I/O and returns Result[T, E].

Reference: RFC-002 §3.1 Configuration Schema
"""

from __future__ import annotations

import sys
from pathlib import Path, PurePath

if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib

from returns.result import Failure, Result, Success

from lattice.core.config import (
    get_default_config,
    parse_project,
    parse_rule_title,
    validate_config,
)
from lattice.core.types.config import Config
from lattice.core.types.evidence import ProjectRegistration
from lattice.core.types.rule import Rule
from lattice.shell.auth import get_auth, load_auth


# @shell_complexity: Security validation with multiple path checks (traversal + sensitive paths + symlink resolution)
def _validate_file_path(path: Path) -> Result[Path, str]:
    """Validate file path is safe for reading.

    Security checks:
    - No path traversal (.. in path)
    - No sensitive system paths (validated on BOTH expanded AND resolved paths)
    - No symlinks pointing to sensitive paths

    Symlink Policy (Option B):
    Allow symlinks but validate the RESOLVED target path is not in blocked locations.
    This permits legitimate symlinks while preventing malicious redirection.

    Note on macOS compatibility:
    On macOS, /etc is a symlink to /private/etc. We check BOTH the user-specified
    path and the resolved path, plus macOS-specific /private/* paths, to ensure
    comprehensive protection.

    Edge cases handled:
    - Symlink chains: resolved recursively via Path.resolve()
    - Broken symlinks: target path is still validated (resolve() returns target)
    - Circular symlinks: caught by OSError from resolve()

    Args:
        path: Path to validate

    Returns:
        Success(resolved_path) if safe, Failure(reason) if not.
        The resolved_path has ~ expanded and symlinks resolved.

    >>> from pathlib import Path
    >>> _validate_file_path(Path("/safe/path")).unwrap()
    PosixPath('/safe/path')
    >>> isinstance(_validate_file_path(Path("../secret")), Failure)
    True
    >>> isinstance(_validate_file_path(Path("/etc/passwd")), Failure)
    True
    >>> isinstance(_validate_file_path(Path("safe/../other")), Failure)
    True
    >>> isinstance(_validate_file_path(Path("file..backup.txt")), Success)
    True
    """
    path_str = str(path)

    # Check for path traversal patterns (path-part-aware)
    # Use PurePath to check if any component is exactly '..'
    # This allows benign filenames like 'file..backup.txt' while blocking actual traversal
    if ".." in PurePath(path).parts:
        return Failure(f"Path traversal not allowed: {path_str}")

    # Expand user home directory (~)
    expanded_path = path.expanduser()
    expanded_str = str(expanded_path)

    # Resolve the path to get the real target (handles symlinks)
    # This is CRITICAL for security: validates where symlink points, not just the link itself
    try:
        resolved_path = expanded_path.resolve()
    except OSError as e:
        # Handle circular symlinks and other resolution errors
        return Failure(f"Path resolution failed: {e}")

    resolved_str = str(resolved_path)

    # Sensitive prefixes - checked on BOTH expanded and resolved paths
    # Includes macOS-specific /private/* paths (where /etc, /var symlink to)
    sensitive_prefixes = [
        "/etc/",
        "/proc/",
        "/sys/",
        "/root/",
        "/private/etc/",  # macOS: /etc -> /private/etc
        "/private/var/db/",  # macOS: system databases
    ]

    # Check BOTH expanded and resolved paths
    # - Expanded: catches user-specified paths like /etc/passwd
    # - Resolved: catches symlinks pointing to sensitive locations
    for check_path in (expanded_str, resolved_str):
        for prefix in sensitive_prefixes:
            if check_path.startswith(prefix) or check_path == prefix.rstrip("/"):
                return Failure("Access to sensitive system paths not allowed")

    # Block sensitive home directory patterns on both paths
    # Must block both exact directory (e.g., /home/user/.ssh) AND contents (e.g., /home/user/.ssh/id_rsa)
    # Pattern: ends with /.ssh OR contains /.ssh/ (same for .gnupg, .pgp)
    home_sensitive = [".ssh", ".gnupg", ".pgp"]
    for check_path in (expanded_str, resolved_str):
        for dirname in home_sensitive:
            # Block exact directory match: /path/.ssh
            # Block contents: /path/.ssh/anything
            if check_path.endswith(f"/{dirname}") or f"/{dirname}/" in check_path:
                return Failure("Access to sensitive home directory paths not allowed")

    # Return the RESOLVED path for safe use in file operations
    # This ensures ~ expansion and symlink resolution are applied
    return Success(resolved_path)


# @shell_complexity: File I/O + TOML parsing + validation with multiple error types
def read_config(path: Path) -> Result[Config, str]:
    """Read and validate configuration from TOML file.

    >>> from pathlib import Path
    >>> import tempfile
    >>> with tempfile.TemporaryDirectory() as tmpdir:
    ...     p = Path(tmpdir) / "config.toml"
    ...     _ = p.write_text('[compiler]\\nmodel = "ollama/llama3"')
    ...     isinstance(read_config(p), Success)
    True
    """
    if not path.exists():
        return Failure(f"Config file not found: {path}")

    try:
        raw = tomllib.loads(path.read_text())
        return Success(validate_config(raw))
    except tomllib.TOMLDecodeError as e:
        return Failure(f"Invalid TOML: {e}")
    except ValueError as e:
        return Failure(f"Invalid config: {e}")
    except Exception as e:
        return Failure(f"Error: {e}")


# @invar:allow shell_result: Returns tuple for easier caller handling, not Result
# @shell_complexity: Multi-path config loading with fallback chain
def load_config_with_fallback(
    project_config_path: Path | None = None,
) -> tuple[Config, str | None]:
    """Load config with fallback chain: project -> global -> default.

    Returns (config, config_path_used) where config_path_used may be None for default.
    This allows commands to use a consistent config loading strategy that respects
    both project-level and global configurations.

    Priority:
    1. Project-level config (if project_config_path provided and has model)
    2. Global config (~/.config/lattice/config.toml if has model)
    3. Default config (empty model - callers should handle)

    Args:
        project_config_path: Path to project-level config.toml (e.g., .lattice/config.toml)

    Returns:
        Tuple of (Config, path_used) where path_used is the config file path or None for default

    >>> from pathlib import Path
    >>> import tempfile
    >>> with tempfile.TemporaryDirectory() as tmpdir:
    ...     p = Path(tmpdir) / "config.toml"
    ...     _ = p.write_text('[compiler]\\nmodel = "test"')
    ...     config, path = load_config_with_fallback(p)
    ...     config.compiler.model
    'test'
    """
    config = None
    config_path_used = None

    # Try project-level config first
    if project_config_path and project_config_path.exists():
        config_result = read_config(project_config_path)
        if isinstance(config_result, Success):
            config = config_result.unwrap()
            config_path_used = str(project_config_path)
            # If project config has model, use it
            if config.compiler.model:
                return config, config_path_used

    # Try global config if project config doesn't have model
    global_dir_result = resolve_global_dir()
    if isinstance(global_dir_result, Success):
        global_config_path = global_dir_result.unwrap() / "config.toml"
        if global_config_path.exists():
            global_config_result = read_config(global_config_path)
            if isinstance(global_config_result, Success):
                global_config = global_config_result.unwrap()
                # Use global config if it has model and project config doesn't
                if global_config.compiler.model and (
                    config is None or not config.compiler.model
                ):
                    return global_config, str(global_config_path)

    # Return project config (may have no model) or default
    if config is not None:
        return config, config_path_used

    return get_default_config(), None


# @shell_complexity: Directory traversal + file reading + rule parsing
def read_rules(rules_dir: Path) -> Result[list[Rule], str]:
    """Read all rule files from a directory.

    >>> from pathlib import Path
    >>> import tempfile
    >>> with tempfile.TemporaryDirectory() as tmpdir:
    ...     d = Path(tmpdir) / "rules"
    ...     d.mkdir()
    ...     _ = (d / "test.md").write_text("# Test\\nContent")
    ...     isinstance(read_rules(d), Success)
    True
    """
    if not rules_dir.exists():
        return Failure(f"Rules directory not found: {rules_dir}")
    if not rules_dir.is_dir():
        return Failure(f"Not a directory: {rules_dir}")

    rules = []
    for fp in sorted(rules_dir.glob("*.md")):
        r = _read_rule(fp)
        if isinstance(r, Success):
            rules.append(r.unwrap())

    return Success(rules)


def _read_rule(path: Path) -> Result[Rule, str]:
    """Read a single rule file."""
    try:
        content = path.read_text()
        title = parse_rule_title(content, path.stem)
        return Success(Rule(file_path=path.name, title=title, content=content))
    except Exception as e:
        return Failure(f"Error reading {path}: {e}")


def resolve_global_dir() -> Result[Path, str]:
    """Resolve the global lattice directory path.

    >>> isinstance(resolve_global_dir(), Success)
    True
    """
    import os

    xdg = os.environ.get("XDG_CONFIG_HOME")
    base = Path(xdg) if xdg else Path.home() / ".config"
    return Success(base / "lattice")


def resolve_project_dir() -> Result[Path, str]:
    """Resolve the project lattice directory path.

    >>> isinstance(resolve_project_dir(), Success)
    True
    """
    for parent in [Path.cwd()] + list(Path.cwd().parents):
        lat = parent / ".lattice"
        if lat.exists() and lat.is_dir():
            return Success(lat)
    return Success(Path.cwd() / ".lattice")


# @shell_complexity: TOML parsing + project registration iteration + validation
def read_projects(path: Path) -> Result[list[ProjectRegistration], str]:
    """Read projects registry from TOML file.

    >>> from pathlib import Path
    >>> import tempfile
    >>> with tempfile.TemporaryDirectory() as tmpdir:
    ...     p = Path(tmpdir) / "projects.toml"
    ...     _ = p.write_text('[[projects]]\\npath = "/a"\\nname = "a"\\nregistered_at = "2026-01-01"')
    ...     isinstance(read_projects(p), Success)
    True
    """
    if not path.exists():
        return Success([])

    try:
        raw = tomllib.loads(path.read_text())
        projects = []
        for p in raw.get("projects", []):
            data = parse_project(p)
            if data:
                projects.append(ProjectRegistration(**data))
        return Success(projects)
    except tomllib.TOMLDecodeError as e:
        return Failure(f"Invalid TOML: {e}")
    except Exception as e:
        return Failure(f"Error: {e}")


# @shell_complexity: Orchestrator for 4 variable syntax types (env/file/auth/direct) - each branch is I/O dispatch
def resolve_api_key(key_input: str | None) -> Result[str | None, str]:
    """Resolve API key from variable syntax.

    Supports:
    - None -> None
    - 'sk-...' (direct) -> 'sk-...'
    - '{env:VAR}' -> os.environ.get('VAR')
    - '{file:/path}' -> Path('/path').read_text().strip()
    - '{auth:provider}' -> load_auth().get('provider', {}).get('api_key')

    Returns Result.success(key) or Result.failure('error message')

    >>> result = resolve_api_key(None)
    >>> result.unwrap() is None
    True

    >>> resolve_api_key('sk-test').unwrap()
    'sk-test'

    >>> resolve_api_key('{env:HOME}').unwrap()  # doctest: +SKIP
    '/home/user'

    >>> import tempfile
    >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:  # doctest: +SKIP
    ...     _ = f.write('my-api-key')
    >>> resolve_api_key(f'{{file:{f.name}}}').unwrap()  # doctest: +SKIP
    'my-api-key'
    """
    import os

    # None input -> None
    if key_input is None:
        return Success(None)

    # Direct key (no variable syntax) -> pass through
    if not (key_input.startswith("{") and key_input.endswith("}")):
        return Success(key_input)

    # Parse variable syntax: {type:value}
    content = key_input[1:-1]  # Remove { and }
    if ":" not in content:
        return Failure(f"Invalid variable syntax: missing ':' in '{key_input}'")

    var_type, var_value = content.split(":", 1)

    if var_type == "env":
        # Environment variable
        value = os.environ.get(var_value)
        if value is None:
            return Failure(f"Environment variable not found: {var_value}")
        return Success(value)

    elif var_type == "file":
        # File path - validate for security before reading
        file_path = Path(var_value)

        # Security: validate path to prevent traversal and sensitive file access
        # Returns the resolved/expanded path for safe file operations
        validation = _validate_file_path(file_path)
        if isinstance(validation, Failure):
            return validation

        # Use the validated/resolved path (handles ~ expansion and symlinks)
        validated_path = validation.unwrap()

        try:
            if not validated_path.exists():
                return Failure(f"File not found: {var_value}")
            content = validated_path.read_text().strip()
            return Success(content)
        except PermissionError:
            return Failure(f"Permission denied reading file: {var_value}")
        except Exception as e:
            return Failure(f"Error reading file {var_value}: {e}")

    elif var_type == "auth":
        # Auth storage
        auth_result = load_auth()
        if isinstance(auth_result, Failure):
            return Failure(f"Failed to load auth: {auth_result.failure()}")

        auth_data = auth_result.unwrap()
        provider_data = auth_data.get(var_value, {})
        api_key = provider_data.get("api_key")

        if api_key is None:
            return Failure(f"No API key found for provider: {var_value}")
        return Success(api_key)

    else:
        return Failure(f"Unknown variable type: {var_type}")


# @shell_complexity: Four-stage priority chain with early returns - each stage is a distinct I/O dispatch
def resolve_api_key_with_fallback(
    config_key: str | None,
    provider: str,
    env_var: str | None = None,
) -> Result[str | None, str]:
    """Resolve API key with full priority chain.

    Priority:
    1. config_key (with variable resolution via resolve_api_key)
    2. auth.json for provider
    3. env_var (direct environment variable lookup)
    4. litellm default handling (returns None)

    Returns Success(key) if found at any priority level, or Success(None)
    if no key found (allowing litellm to use its default behavior).

    >>> resolve_api_key_with_fallback('sk-config', 'openai').unwrap()
    'sk-config'

    >>> import os
    >>> os.environ['MY_TEST_KEY'] = 'secret'
    >>> resolve_api_key_with_fallback('{env:MY_TEST_KEY}', 'openai').unwrap()
    'secret'

    >>> resolve_api_key_with_fallback(None, 'openai', 'OPENAI_API_KEY').unwrap()  # doctest: +SKIP
    'env-key'

    >>> result = resolve_api_key_with_fallback(None, 'unknown_provider', None)
    >>> result.unwrap() is None
    True
    """
    import os

    # Priority 1: config_key (with variable resolution)
    if config_key is not None:
        resolve_result = resolve_api_key(config_key)
        if isinstance(resolve_result, Failure):
            return resolve_result
        key = resolve_result.unwrap()
        if key is not None:
            return Success(key)
        # resolve_api_key returned Success(None), continue to next priority

    # Priority 2: auth.json for provider
    auth_result = get_auth(provider)
    if isinstance(auth_result, Failure):
        return auth_result
    auth_key = auth_result.unwrap()
    if auth_key is not None:
        return Success(auth_key)

    # Priority 3: env_var (direct environment variable lookup)
    if env_var is not None:
        env_value = os.environ.get(env_var)
        if env_value is not None:
            return Success(env_value)

    # Priority 4: litellm default handling (return None)
    return Success(None)


__all__ = [
    "read_config",
    "read_rules",
    "resolve_global_dir",
    "resolve_project_dir",
    "read_projects",
    "get_default_config",
    "resolve_api_key",
    "resolve_api_key_with_fallback",
    "load_config_with_fallback",
]
