"""
Infospace composition model.

Allows one infospace to use another as a discipline — a reusable
framework of concepts applied as an analytical lens.

Key operations:
- Resolve and validate discipline bindings
- Check discipline viability (must meet its own thresholds)
- List discipline entities as mapping targets
- Detect stale mappings when discipline content changes
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

from markitect.infospace.config import (
    DisciplineBinding,
    InfospaceConfig,
    load_infospace_config,
)
from markitect.infospace.entity_parser import parse_entity_directory
from markitect.infospace.history import get_latest_snapshot, read_metrics_file
from markitect.infospace.models import EntityMeta
from markitect.infospace.state import InfospaceState, ViabilityResult, build_state


@dataclass
class DisciplineStatus:
    """Status of a bound discipline infospace."""

    name: str
    path: str
    resolved_path: Optional[Path] = None
    exists: bool = False
    has_config: bool = False
    entity_count: int = 0
    is_viable: bool = False
    viability_results: List[ViabilityResult] = field(default_factory=list)
    error: str = ""

    def to_dict(self) -> Dict[str, Any]:
        d: Dict[str, Any] = {
            "name": self.name,
            "path": self.path,
            "exists": self.exists,
            "has_config": self.has_config,
            "entity_count": self.entity_count,
            "is_viable": self.is_viable,
        }
        if self.viability_results:
            d["viability"] = [r.to_dict() for r in self.viability_results]
        if self.error:
            d["error"] = self.error
        return d


@dataclass
class StaleMappingInfo:
    """Information about a mapping that may be stale."""

    entity_slug: str
    discipline_entity: str
    reason: str

    def to_dict(self) -> Dict[str, Any]:
        return {
            "entity_slug": self.entity_slug,
            "discipline_entity": self.discipline_entity,
            "reason": self.reason,
        }


# ── Resolution ───────────────────────────────────────────────────────


def resolve_discipline_path(
    binding: DisciplineBinding, root: Path
) -> Optional[Path]:
    """Resolve a discipline binding to an absolute path.

    Tries the binding's path relative to *root*, then as an absolute path.
    Returns ``None`` if the directory doesn't exist.
    """
    if not binding.path:
        return None

    # Try relative to root first
    candidate = root / binding.path
    if candidate.is_dir():
        return candidate.resolve()

    # Try as absolute
    candidate = Path(binding.path)
    if candidate.is_dir():
        return candidate.resolve()

    return None


def load_discipline_config(
    binding: DisciplineBinding, root: Path
) -> Optional[InfospaceConfig]:
    """Load the infospace config for a bound discipline.

    Returns ``None`` if the discipline path cannot be resolved or
    has no ``infospace.yaml``.
    """
    disc_path = resolve_discipline_path(binding, root)
    if disc_path is None:
        return None

    config_file = disc_path / "infospace.yaml"
    if not config_file.is_file():
        return None

    return load_infospace_config(config_file)


# ── Viability checking ───────────────────────────────────────────────


def check_discipline_status(
    binding: DisciplineBinding, root: Path
) -> DisciplineStatus:
    """Check the full status of a bound discipline.

    Resolves the path, loads config, counts entities, and checks
    viability against the discipline's own thresholds.
    """
    status = DisciplineStatus(name=binding.name, path=binding.path)

    disc_path = resolve_discipline_path(binding, root)
    if disc_path is None:
        status.error = f"Path not found: {binding.path}"
        return status

    status.resolved_path = disc_path
    status.exists = True

    # Load config
    config_file = disc_path / "infospace.yaml"
    if not config_file.is_file():
        status.error = "No infospace.yaml found"
        return status

    disc_config = load_infospace_config(config_file)
    status.has_config = True

    # Count entities
    entities_dir = disc_path / disc_config.entities_dir
    if entities_dir.is_dir():
        entities = parse_entity_directory(entities_dir)
        status.entity_count = len(entities)

    # Check viability
    if disc_config.viability:
        metrics = read_metrics_file(disc_path / disc_config.metrics_dir / "metrics.yaml")
        if metrics:
            state = build_state(disc_config, metrics=metrics)
            status.viability_results = state.viability_results
            status.is_viable = state.is_viable

    return status


def get_discipline_entities(
    binding: DisciplineBinding, root: Path
) -> List[EntityMeta]:
    """Get all entities from a bound discipline infospace."""
    disc_path = resolve_discipline_path(binding, root)
    if disc_path is None:
        return []

    disc_config = load_discipline_config(binding, root)
    if disc_config is None:
        return []

    entities_dir = disc_path / disc_config.entities_dir
    if not entities_dir.is_dir():
        return []

    return parse_entity_directory(entities_dir)


# ── Stale mapping detection ─────────────────────────────────────────


def _content_digest(entity: EntityMeta) -> str:
    """Compute a short content digest for an entity."""
    content = f"{entity.slug}|{entity.definition}|{entity.domain}"
    return hashlib.sha256(content.encode()).hexdigest()[:12]


def compute_discipline_digests(
    binding: DisciplineBinding, root: Path
) -> Dict[str, str]:
    """Compute content digests for all entities in a discipline.

    Returns ``{slug: digest}`` mapping.
    """
    entities = get_discipline_entities(binding, root)
    return {e.slug: _content_digest(e) for e in entities}


def find_stale_mappings(
    config: InfospaceConfig,
    root: Path,
    mapping_references: Optional[Dict[str, List[str]]] = None,
) -> List[StaleMappingInfo]:
    """Find mappings that may be stale due to discipline changes.

    Args:
        config: The infospace configuration.
        root: Project root directory.
        mapping_references: ``{entity_slug: [discipline_entity_slugs]}``
            mapping of local entities to the discipline entities they
            reference. If ``None``, returns an empty list (no mapping
            data available).

    Returns:
        List of stale mapping info objects.
    """
    if not mapping_references:
        return []

    stale: List[StaleMappingInfo] = []

    for binding in config.disciplines:
        disc_entities = get_discipline_entities(binding, root)
        disc_slugs = {e.slug for e in disc_entities}

        for entity_slug, refs in mapping_references.items():
            for ref_slug in refs:
                if ref_slug not in disc_slugs:
                    stale.append(StaleMappingInfo(
                        entity_slug=entity_slug,
                        discipline_entity=ref_slug,
                        reason=f"Discipline entity '{ref_slug}' no longer exists in '{binding.name}'",
                    ))

    return stale


# ── Binding management ───────────────────────────────────────────────


def bind_discipline(
    config: InfospaceConfig,
    name: str,
    path: str,
    root: Path,
) -> DisciplineStatus:
    """Add a discipline binding to the config and validate it.

    Does NOT persist the config — the caller should save it.

    Args:
        config: The infospace configuration to update.
        name: Discipline name.
        path: Path to the discipline infospace.
        root: Project root for path resolution.

    Returns:
        Status of the newly bound discipline.
    """
    # Check for duplicates
    existing = {d.name for d in config.disciplines}
    if name in existing:
        return DisciplineStatus(
            name=name, path=path, error=f"Discipline '{name}' already bound"
        )

    binding = DisciplineBinding(name=name, path=path)
    config.disciplines.append(binding)

    return check_discipline_status(binding, root)
