"""Space file parser and manager.

Spaces are YAML-based workspace definitions stored in ``~/.anteroom/spaces/``.
Each ``.yaml`` file defines repos, pack sources, packs, sources, instructions,
and config overrides for a named workspace.
"""

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__)

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


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


@dataclass(frozen=True)
class SpaceSource:
    path: str | None = None
    url: 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 = ""
    paths: dict[str, str] = field(default_factory=dict)


def get_spaces_dir() -> Path:
    return Path.home() / ".anteroom" / "spaces"


def list_space_files() -> list[Path]:
    d = get_spaces_dir()
    if not d.is_dir():
        return []
    return sorted(p for p in d.glob("*.yaml") if not p.name.endswith(".local.yaml"))


def file_hash(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def resolve_local_path(space_path: Path) -> Path | None:
    local = space_path.with_suffix("").with_suffix(".local.yaml")
    return local if local.is_file() else None


def parse_space_file(path: Path) -> SpaceConfig:
    if not path.is_file():
        raise FileNotFoundError(f"Space file not found: {path}")
    if path.stat().st_size > _MAX_FILE_SIZE:
        raise ValueError(f"Space file exceeds {_MAX_FILE_SIZE // 1024}KB limit: {path}")

    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: {path}")

    name = raw.get("name", "")
    if not name or not _NAME_PATTERN.match(name):
        raise ValueError(f"Invalid space name: {name!r}")

    pack_sources = []
    for ps in raw.get("pack_sources", []):
        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 = []
    for src in raw.get("sources", []):
        if isinstance(src, str):
            sources.append(SpaceSource(path=src))
        elif isinstance(src, dict):
            sources.append(SpaceSource(path=src.get("path"), url=src.get("url")))

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


def parse_local_file(path: Path) -> SpaceLocalConfig:
    if not path.is_file():
        raise FileNotFoundError(f"Local config file not found: {path}")

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

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


def write_space_file(path: Path, config: SpaceConfig) -> None:
    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} for ps in config.pack_sources]
    if config.packs:
        data["packs"] = config.packs
    if config.sources:
        data["sources"] = [{k: v for k, v in [("path", s.path), ("url", s.url)] if v} 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:
    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 get_space_config_overlay(space_file_path: Path) -> dict[str, Any]:
    """Extract the ``config`` overlay dict from a space file.

    Returns an empty dict if the file doesn't exist, is invalid, or has no
    ``config`` section.
    """
    try:
        cfg = parse_space_file(space_file_path)
        return dict(cfg.config) if cfg.config else {}
    except Exception:
        logger.warning("Could not read space config overlay from %s", space_file_path)
        return {}


def validate_space(config: SpaceConfig) -> list[str]:
    errors: list[str] = []

    if not _NAME_PATTERN.match(config.name):
        errors.append(f"Invalid space name: {config.name!r}")

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

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

    for src in config.sources:
        if src.path:
            if ".." in src.path.split("/"):
                errors.append(f"sources: path traversal not allowed: {src.path}")
        if src.url:
            err = _validate_url_scheme(src.url)
            if err:
                errors.append(f"sources: {err}")

    return errors
