"""Space file parser and manager.

Spaces are YAML-based workspace definitions that bundle repos, pack sources,
packs, sources, instructions, and config overrides into a single file.
"""

from __future__ import annotations

import hashlib
import logging
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml

from .pack_sources import _validate_url_scheme

logger = logging.getLogger(__name__)

_MAX_SPACE_FILE_SIZE = 256 * 1024  # 256 KB
_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$")
_PACK_REF_PATTERN = re.compile(r"^[a-z0-9_-]+/[a-z0-9_-]+$")


@dataclass(frozen=True)
class SpacePackSource:
    url: str
    branch: str = "main"


@dataclass(frozen=True)
class SpaceSource:
    type: str
    title: str
    url: str | None = None
    content: str | None = None


@dataclass(frozen=True)
class SpaceConfig:
    name: str
    version: str = "1"
    repos: list[str] = field(default_factory=list)
    pack_sources: list[SpacePackSource] = field(default_factory=list)
    packs: list[str] = field(default_factory=list)
    sources: list[SpaceSource] = field(default_factory=list)
    instructions: str = ""
    config: dict[str, Any] = field(default_factory=dict)


@dataclass
class SpaceLocalConfig:
    repos_root: str | None = None
    paths: dict[str, str] = field(default_factory=dict)


def parse_space_file(path: Path) -> SpaceConfig:
    """Parse a space YAML file into a SpaceConfig."""
    if not path.exists():
        raise FileNotFoundError(f"Space file not found: {path}")

    size = path.stat().st_size
    if size > _MAX_SPACE_FILE_SIZE:
        raise ValueError(f"Space file too large: {size} bytes (max {_MAX_SPACE_FILE_SIZE})")

    raw = yaml.safe_load(path.read_text(encoding="utf-8"))
    if not isinstance(raw, dict):
        raise ValueError(f"Space file must be a YAML mapping, got {type(raw).__name__}")

    name = raw.get("name")
    if not name:
        raise ValueError("Space file must have a 'name' field")
    if not _NAME_PATTERN.match(name):
        raise ValueError(f"Invalid space name: {name!r} (must match {_NAME_PATTERN.pattern})")

    pack_sources_raw = raw.get("pack_sources", [])
    pack_sources: list[SpacePackSource] = []
    for ps in pack_sources_raw:
        if isinstance(ps, str):
            pack_sources.append(SpacePackSource(url=ps))
        elif isinstance(ps, dict):
            pack_sources.append(SpacePackSource(url=ps["url"], branch=ps.get("branch", "main")))

    sources_raw = raw.get("sources", [])
    sources: list[SpaceSource] = []
    for src in sources_raw:
        if isinstance(src, dict):
            sources.append(
                SpaceSource(
                    type=src.get("type", "text"),
                    title=src.get("title", ""),
                    url=src.get("url"),
                    content=src.get("content"),
                )
            )

    return SpaceConfig(
        name=name,
        version=str(raw.get("version", "1")),
        repos=raw.get("repos", []),
        pack_sources=pack_sources,
        packs=raw.get("packs", []),
        sources=sources,
        instructions=raw.get("instructions", ""),
        config=raw.get("config", {}),
    )


def parse_local_file(path: Path) -> SpaceLocalConfig:
    """Parse a space .local.yaml file."""
    if not path.exists():
        return SpaceLocalConfig()

    raw = yaml.safe_load(path.read_text(encoding="utf-8"))
    if not isinstance(raw, dict):
        return SpaceLocalConfig()

    return SpaceLocalConfig(
        repos_root=raw.get("repos_root"),
        paths=raw.get("paths", {}),
    )


def write_space_file(path: Path, config: SpaceConfig) -> None:
    """Write a SpaceConfig to a YAML file."""
    data: dict[str, Any] = {"name": config.name, "version": config.version}
    if config.repos:
        data["repos"] = config.repos
    if config.pack_sources:
        data["pack_sources"] = [
            {"url": ps.url, "branch": ps.branch} if ps.branch != "main" else ps.url for ps in config.pack_sources
        ]
    if config.packs:
        data["packs"] = config.packs
    if config.sources:
        data["sources"] = [
            {"type": s.type, "title": s.title, "url": s.url, "content": s.content} for s in config.sources
        ]
    if config.instructions:
        data["instructions"] = config.instructions
    if config.config:
        data["config"] = config.config

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8")


def write_local_file(path: Path, local: SpaceLocalConfig) -> None:
    """Write a SpaceLocalConfig to a .local.yaml file."""
    data: dict[str, Any] = {}
    if local.repos_root:
        data["repos_root"] = local.repos_root
    if local.paths:
        data["paths"] = local.paths

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8")


def validate_space(config: SpaceConfig) -> list[str]:
    """Validate a space config. Returns list of error strings (empty = valid)."""
    errors: list[str] = []

    for url in config.repos:
        url_err = _validate_url_scheme(url)
        if url_err:
            errors.append(f"repo URL: {url_err}")

    for ps in config.pack_sources:
        url_err = _validate_url_scheme(ps.url)
        if url_err:
            errors.append(f"pack_source URL: {url_err}")

    for pack_ref in config.packs:
        if not _PACK_REF_PATTERN.match(pack_ref):
            errors.append(f"Invalid pack reference: {pack_ref!r} (expected namespace/name)")

    if config.instructions:
        if "/../" in config.instructions or config.instructions.startswith("../"):
            errors.append("instructions contain path traversal pattern")

    for src in config.sources:
        if src.url:
            url_err = _validate_url_scheme(src.url)
            if url_err:
                errors.append(f"source URL: {url_err}")

    if config.config:
        _check_config_paths(config.config, errors)

    return errors


def _check_config_paths(config: dict[str, Any], errors: list[str], prefix: str = "") -> None:
    """Check config values for path traversal."""
    for key, val in config.items():
        full_key = f"{prefix}.{key}" if prefix else key
        if isinstance(val, str):
            if "/../" in val or val.startswith("../"):
                errors.append(f"config path traversal in {full_key}: {val!r}")
            if val.startswith("/") and not val.startswith("/tmp"):
                errors.append(f"config absolute path in {full_key}: {val!r}")
        elif isinstance(val, dict):
            _check_config_paths(val, errors, full_key)


def file_hash(path: Path) -> str:
    """Compute SHA-256 hash of a file."""
    return hashlib.sha256(path.read_bytes()).hexdigest()


def get_spaces_dir() -> Path:
    """Return the spaces directory (~/.anteroom/spaces/)."""
    return Path.home() / ".anteroom" / "spaces"


def list_space_files() -> list[Path]:
    """List all space YAML files in the spaces directory."""
    spaces_dir = get_spaces_dir()
    if not spaces_dir.exists():
        return []
    return sorted(p for p in spaces_dir.glob("*.yaml") if not p.name.endswith(".local.yaml"))


def resolve_local_path(space_path: Path) -> Path:
    """Resolve the .local.yaml path for a space file."""
    return space_path.with_suffix(".local.yaml")
