"""Shared build pipeline for template generation.

All entrypoints (CLI, MCP, GitHub workflows) call ``build()`` instead
of implementing their own token → IR → emit → validate flow.

Routes through ``DesignResolutionEngine`` (IR Path B) for richer
typography with OpenType features, baseline-grid-snapped spacing,
and modular type scales.

See ADR 027 (F2) and the BuildPipeline spec.
"""

from __future__ import annotations

import json
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime as _datetime
from datetime import timezone
from pathlib import Path
from typing import Any, Literal

from tokenmoulds import __version__ as _PIPELINE_VERSION
from tokenmoulds.dtcg.converter import convert_from_dtcg
from tokenmoulds.dtcg.font_metrics import (
    extract_font_metrics,
)
from tokenmoulds.dtcg.generator import TokenGenerator
from tokenmoulds.dtcg.layered_adapter import adapt_dtcg_layers
from tokenmoulds.dtcg.parser import parse_tokens
from tokenmoulds.dtcg.reference_resolver import resolve_references
from tokenmoulds.design.engine import (
    DesignResolutionEngine,
    LineHeightMode,
)
from tokenmoulds.ir import DocumentIR, build_document_ir as _build_document_ir_legacy
from tokenmoulds.ir.design_bridge import (
    create_document_ir_from_design,
)

import structlog

logger = structlog.get_logger(__name__)


# =============================================================================
# Configuration and Result Types
# =============================================================================


TokenSource = Literal[
    "payload", "brand_inputs", "layers", "tokens_file", "github", "unknown"
]


@dataclass
class BuildConfig:
    """Configuration for a single build pipeline invocation.

    Provide exactly one token source: ``token_payload``, ``tokens_file``,
    ``layers_dir`` / ``layers_manifest``, or ``brand_inputs``.
    """

    # Required
    org_id: str
    output_formats: list[str]

    # Token source (one of these)
    token_payload: dict | None = None
    tokens_file: Path | None = None
    layers_dir: Path | None = None
    layers_manifest: Path | None = None
    brand_inputs: dict | None = None

    # Build controls
    locale: str = "US"
    template_root: Path | None = None
    fonts_dir: Path | None = None
    embed_fonts: bool = True
    validate: bool = True

    # Design engine controls
    policy: str = "default"
    base_font_size_pt: float = 11.0
    base_grid_pt: float = 0  # 0 = auto-select from font metrics

    # GitHub brand source (optional — ADR 026 F3)
    repository: str | None = None
    ref: str | None = None
    manifest_path: str | None = None

    # License (optional — free tier works without)
    license_id: str | None = None
    license_repo: str | None = None

    def token_source_label(self) -> TokenSource:
        """Canonical label for provenance; mirrors _resolve_tokens() priority."""
        if self.repository is not None:
            return "github"
        if self.token_payload is not None:
            return "payload"
        if self.brand_inputs is not None:
            return "brand_inputs"
        if self.layers_dir is not None or self.layers_manifest is not None:
            return "layers"
        if self.tokens_file is not None:
            return "tokens_file"
        return "unknown"


@dataclass
class BuildProvenance:
    """Build metadata for reproducibility and audit."""

    org_id: str
    timestamp: str  # ISO 8601
    token_source: TokenSource
    output_formats: list[str] = field(default_factory=list)
    pipeline_version: str = ""
    timing_ms: float = 0


@dataclass
class BuildResult:
    """Result of a build pipeline invocation."""

    success: bool
    artifacts: dict[str, bytes] = field(default_factory=dict)
    validation_results: dict[str, list] = field(default_factory=dict)
    document_ir: DocumentIR | None = None
    token_payload: dict | None = None
    provenance: BuildProvenance | None = None
    error: str | None = None
    timing_ms: float = 0

    def to_build_result_dict(self) -> dict:
        """Serialize for build-result.json."""
        result: dict[str, Any] = {
            "success": self.success,
            "timing_ms": self.timing_ms,
            "formats": sorted(self.artifacts.keys()),
            "artifact_sizes": {fmt: len(data) for fmt, data in self.artifacts.items()},
        }
        if self.error:
            result["error"] = self.error
        if self.provenance:
            result["provenance"] = asdict(self.provenance)
        if self.validation_results:
            result["validation_summary"] = {
                fmt: len(issues) for fmt, issues in self.validation_results.items()
            }
        return result

    def to_validation_report_dict(self) -> dict:
        """Serialize for validation-report.json."""
        return {
            "success": self.success,
            "formats_validated": sorted(self.validation_results.keys()),
            "total_issues": sum(len(v) for v in self.validation_results.values()),
            "results": {
                fmt: issues for fmt, issues in sorted(self.validation_results.items())
            },
        }

    def to_build_result_json(self, indent: int = 2) -> str:
        """Serialize build result as JSON string."""
        return json.dumps(self.to_build_result_dict(), indent=indent)

    def to_validation_report_json(self, indent: int = 2) -> str:
        """Serialize validation report as JSON string."""
        return json.dumps(self.to_validation_report_dict(), indent=indent)


# =============================================================================
# Format → Emitter Dispatch
# =============================================================================

# Maps output format extension to (module_path, class_name, supports_fonts)
_EMITTER_REGISTRY: dict[str, tuple[str, str, bool]] = {
    "potx": ("tokenmoulds.emitters.powerpoint", "PowerPointEmitter", True),
    "dotx": ("tokenmoulds.emitters.word", "WordDocumentEmitter", True),
    "xltx": ("tokenmoulds.emitters.excel", "ExcelEmitter", False),
    "otp": ("tokenmoulds.emitters.odf", "ImpressTemplateEmitter", False),
    "ott": ("tokenmoulds.emitters.odf", "WriterTemplateEmitter", False),
    "ots": ("tokenmoulds.emitters.odf", "CalcTemplateEmitter", False),
    "odg": ("tokenmoulds.emitters.odf", "DrawTemplateEmitter", False),
    "gdocs": ("tokenmoulds.emitters.google_docs", "GoogleDocsEmitter", True),
    "gdoc": ("tokenmoulds.emitters.google_docs", "GoogleDocsDocumentEmitter", False),
}


def _get_emitter_class(fmt: str):
    """Lazy-import and return the emitter class for a format."""
    import importlib

    if fmt not in _EMITTER_REGISTRY:
        raise ValueError(
            f"Unknown output format: {fmt!r}. "
            f"Available: {sorted(_EMITTER_REGISTRY.keys())}"
        )
    module_path, class_name, _ = _EMITTER_REGISTRY[fmt]
    module = importlib.import_module(module_path)
    return getattr(module, class_name)


# =============================================================================
# Token Resolution
# =============================================================================


def _resolve_tokens(config: BuildConfig, _depth: int = 0) -> dict:
    """Resolve tokens from whichever source is provided in config."""
    if config.repository is not None:
        if _depth > 0:
            raise ValueError("Recursive GitHub resolution not supported")

        import shutil

        from tokenmoulds.brand.github import GitHubSource, resolve_github_source
        from tokenmoulds.brand.manifest import manifest_to_build_config

        source = GitHubSource(
            repository=config.repository,
            ref=config.ref or "main",
            path=config.manifest_path or "brand-manifest.yaml",
        )
        manifest, tmp_dir = resolve_github_source(source)
        try:
            derived_config = manifest_to_build_config(manifest, tmp_dir)
            return _resolve_tokens(derived_config, _depth + 1)
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)

    if config.token_payload is not None:
        return config.token_payload

    if config.brand_inputs is not None:
        return TokenGenerator().generate(config.brand_inputs)

    if config.layers_dir is not None or config.layers_manifest is not None:
        result = adapt_dtcg_layers(
            layers_dir=config.layers_dir,
            manifest_path=config.layers_manifest,
        )
        return result.payload

    if config.tokens_file is not None:
        token_file = parse_tokens(config.tokens_file)
        resolved, _resolutions, _warnings = resolve_references(token_file, strict=False)
        return convert_from_dtcg(resolved)

    raise ValueError(
        "BuildConfig must provide one of: token_payload, tokens_file, "
        "layers_dir/layers_manifest, or brand_inputs"
    )


# =============================================================================
# IR Construction (Path B — DesignResolutionEngine)
# =============================================================================


def _extract_font_pair(tokens: dict) -> dict[str, str]:
    """Extract font_pair from resolved tokens."""
    typography = tokens.get("typography", {})
    fonts = typography.get("fonts", {})
    if fonts:
        return dict(fonts)
    # Fall back to inputs section
    inputs = tokens.get("inputs", {})
    font_pair = inputs.get("font_pair", {})
    if font_pair:
        return dict(font_pair)
    return {"sans": "Inter", "serif": "Inter"}


def _extract_base_colors(tokens: dict) -> dict[str, str]:
    """Extract base colors from resolved tokens."""
    inputs = tokens.get("inputs", {})
    base_colors = inputs.get("base_colors", {})
    if base_colors:
        return dict(base_colors)
    colors = tokens.get("colors", {})
    palette = colors.get("palette", {})
    primary = palette.get("primary", {}).get("base", "#2563EB")
    secondary = palette.get("secondary", {}).get("base", "#DC2626")
    return {"primary": primary, "secondary": secondary}


def _extract_brand_tone(tokens: dict) -> float:
    """Extract brand_tone from resolved tokens."""
    inputs = tokens.get("inputs", {})
    return float(inputs.get("brand_tone", tokens.get("brand_tone", 0.5)))


def _extract_content_density(tokens: dict) -> float:
    """Extract content_density from resolved tokens."""
    inputs = tokens.get("inputs", {})
    return float(inputs.get("content_density", 0.4))


def _build_color_theme(tokens: dict):
    """Build ColorTheme from resolved token payload."""
    from tokenmoulds.ir import ColorTheme
    from tokenmoulds.design.color_defaults import (
        DEFAULT_PRIMARY,
        DEFAULT_SECONDARY,
        DEFAULT_THEME_MAPPING,
    )

    colors = tokens.get("colors", {})
    theme_map = colors.get("theme_mapping", {})
    if not theme_map:
        theme_map = dict(DEFAULT_THEME_MAPPING)
        theme_map["accent1"] = (
            colors.get("palette", {}).get("primary", {}).get("base", DEFAULT_PRIMARY)
        )
        theme_map["accent2"] = (
            colors.get("palette", {})
            .get("secondary", {})
            .get("base", DEFAULT_SECONDARY)
        )

    return ColorTheme(
        dk1=theme_map["dk1"],
        lt1=theme_map["lt1"],
        dk2=theme_map["dk2"],
        lt2=theme_map["lt2"],
        accent1=theme_map["accent1"],
        accent2=theme_map["accent2"],
        accent3=theme_map["accent3"],
        accent4=theme_map["accent4"],
        accent5=theme_map["accent5"],
        accent6=theme_map["accent6"],
        hyperlink=theme_map.get("hlink", theme_map["accent1"]),
        hyperlink_followed=theme_map.get("folHlink", theme_map["accent2"]),
    )


def _build_document_ir(
    tokens: dict,
    config: BuildConfig,
) -> DocumentIR:
    """Build DocumentIR via DesignResolutionEngine (Path B).

    Falls back to legacy Path A if Path B fails (e.g., missing font
    metrics), so the pipeline never produces worse output than before.
    """
    font_pair = _extract_font_pair(tokens)
    brand_tone = _extract_brand_tone(tokens)
    content_density = _extract_content_density(tokens)
    color_theme = _build_color_theme(tokens)

    try:
        font_metrics = extract_font_metrics(
            font_pair=font_pair,
            font_metrics_cache=None,
            fonts_dir=str(config.fonts_dir) if config.fonts_dir else None,
        )
    except (ValueError, RuntimeError) as exc:
        logger.warning(
            "Font metrics unavailable, falling back to legacy IR builder",
            error=str(exc),
        )
        return _build_document_ir_legacy(tokens, config.org_id, config.locale)

    # Build DesignResolutionEngine inputs
    from tokenmoulds.dtcg.generator import (
        _BrandAdapter,
        _FontConfigAdapter,
        _FontMetricsAdapter,
        _select_scale_ratio,
    )
    from tokenmoulds.fonts.superfamilies import resolve_monospace

    primary_metrics = font_metrics["primary"]
    serif_metrics = font_metrics.get("serif", primary_metrics)

    body_family = (
        font_pair.get("body")
        or font_pair.get("minor")
        or font_pair.get("sans")
        or "Inter"
    )
    heading_family = (
        font_pair.get("heading")
        or font_pair.get("major")
        or font_pair.get("serif")
        or body_family
    )
    mono_family = font_pair.get("mono") or "Courier New"
    try:
        mono_family = resolve_monospace(
            body_family, heading_family, font_pair.get("mono")
        )
    except Exception:
        pass

    body_adapter = _FontMetricsAdapter(
        x_height=primary_metrics["x_height"],
        units_per_em=primary_metrics["units_per_em"],
        cap_height=primary_metrics["cap_height"],
        ascender=primary_metrics["ascender"],
        descender=primary_metrics["descender"],
    )
    heading_adapter = _FontMetricsAdapter(
        x_height=serif_metrics["x_height"],
        units_per_em=serif_metrics["units_per_em"],
        cap_height=serif_metrics["cap_height"],
        ascender=serif_metrics["ascender"],
        descender=serif_metrics["descender"],
    )
    font_config = _FontConfigAdapter(
        body=body_adapter,
        heading=heading_adapter,
        _body_family=body_family,
        _heading_family=heading_family,
        _mono_family=mono_family,
    )
    brand = _BrandAdapter(tone=brand_tone, density=content_density)
    scale_ratio = _select_scale_ratio(brand_tone)

    engine = DesignResolutionEngine(
        fonts=font_config,
        brand=brand,
        locale=config.locale,
        colors={},
        base_font_size_pt=config.base_font_size_pt,
        scale_ratio=scale_ratio,
        line_height_mode=LineHeightMode.SNAPPED,
    )
    design = engine.resolve()

    return create_document_ir_from_design(
        design,
        org_id=config.org_id,
        locale=config.locale,
        body_font=body_family,
        heading_font=heading_family,
        monospace_font=mono_family,
        color_theme=color_theme,
    )


# =============================================================================
# Emission
# =============================================================================


def _create_style_resolver(tokens: dict, document_ir: DocumentIR):
    """Create a StyleResolver for Word formats."""
    try:
        from tokenmoulds.styles.factory import create_style_resolver_from_tokens

        body_color = document_ir.body_style.color
        heading_color = body_color
        if document_ir.heading_styles:
            heading_color = document_ir.heading_styles[0].color

        return create_style_resolver_from_tokens(
            tokens, body_color=body_color, heading_color=heading_color
        )
    except Exception as exc:
        logger.debug("StyleResolver creation failed: %s", exc)
        return None


def _emit_format(
    fmt: str,
    document_ir: DocumentIR,
    tokens: dict,
    config: BuildConfig,
) -> bytes:
    """Emit a single format and return raw bytes."""
    emitter_cls = _get_emitter_class(fmt)
    _, _, supports_fonts = _EMITTER_REGISTRY[fmt]

    kwargs: dict[str, Any] = {}
    if config.template_root is not None:
        kwargs["template_root"] = str(config.template_root)

    if supports_fonts:
        kwargs["embed_fonts"] = config.embed_fonts
        if config.fonts_dir:
            kwargs["fonts_dir"] = config.fonts_dir

    # Word formats need a StyleResolver
    if fmt in ("dotx", "ott"):
        resolver = _create_style_resolver(tokens, document_ir)
        if resolver is not None:
            kwargs["style_resolver"] = resolver

    emitter = emitter_cls(document_ir, **kwargs)
    try:
        return emitter.build_package()
    except Exception:
        if "style_resolver" in kwargs:
            # Retry without StyleResolver if it caused the failure
            del kwargs["style_resolver"]
            emitter = emitter_cls(document_ir, **kwargs)
            return emitter.build_package()
        raise


# =============================================================================
# Validation
# =============================================================================


def _validate_artifact(fmt: str, data: bytes) -> list:
    """Validate a template artifact in-process. Returns list of issues."""
    try:
        from openxml_audit import FileFormat, OpenXmlValidator, ValidationSeverity

        ooxml_formats = {"potx", "dotx", "xltx", "thmx"}
        if fmt not in ooxml_formats:
            return []

        import tempfile

        with tempfile.NamedTemporaryFile(suffix=f".{fmt}", delete=False) as f:
            f.write(data)
            tmp_path = f.name

        try:
            validator = OpenXmlValidator(file_format=FileFormat.OFFICE_2019)
            result = validator.validate(tmp_path)
            return [
                {
                    "type": str(e.error_type),
                    "description": e.description,
                    "part": e.part_uri,
                    "path": e.path,
                    "severity": str(e.severity),
                }
                for e in result.errors
                if e.severity == ValidationSeverity.ERROR
            ]
        finally:
            Path(tmp_path).unlink(missing_ok=True)

    except ImportError:
        logger.debug("openxml_audit not available, skipping validation for %s", fmt)
        return []
    except Exception as exc:
        logger.warning("Validation failed for %s: %s", fmt, exc)
        return [{"type": "error", "description": str(exc)}]


# =============================================================================
# Main Build Function
# =============================================================================


def build(config: BuildConfig) -> BuildResult:
    """Execute the canonical build pipeline.

    Steps:
        1. Resolve tokens from whichever source is provided
        2. Build DocumentIR via DesignResolutionEngine (Path B)
        3. For each output format: emit → validate
        4. Return BuildResult with all artifacts

    All entrypoints (CLI, MCP, GitHub workflows) should call this
    function instead of implementing their own build flow.
    """
    start = time.monotonic()
    build_timestamp = _datetime.now(timezone.utc).isoformat()

    try:
        # Step 0: License check (optional — free tier works without)
        if config.license_id:
            try:
                from tokenmoulds.licensing import (
                    LicenseValidationError,
                    resolve_license_context,
                )
                from tokenmoulds.licensing.client import LicenseValidator
            except ImportError as exc:
                return BuildResult(
                    success=False,
                    error=f"Licensing module not available: {exc}",
                    timing_ms=(time.monotonic() - start) * 1000,
                )
            try:
                license_ctx = resolve_license_context(
                    license_id=config.license_id,
                    license_repo=config.license_repo,
                )
                license_result = LicenseValidator().validate(license_ctx)
                logger.info(
                    "License validated",
                    license_id=license_result.license_id,
                    plan=license_result.plan,
                )
            except LicenseValidationError as exc:
                return BuildResult(
                    success=False,
                    error=f"License validation failed: {exc}",
                    timing_ms=(time.monotonic() - start) * 1000,
                )

        # Step 1: Resolve tokens
        tokens = _resolve_tokens(config)

        # Step 2: Build IR via Path B (DesignResolutionEngine)
        document_ir = _build_document_ir(tokens, config)

        # Step 3: Emit each format
        artifacts: dict[str, bytes] = {}
        validation_results: dict[str, list] = {}

        for fmt in config.output_formats:
            try:
                data = _emit_format(fmt, document_ir, tokens, config)
                artifacts[fmt] = data

                # Step 4: Validate if requested
                if config.validate:
                    issues = _validate_artifact(fmt, data)
                    if issues:
                        validation_results[fmt] = issues
            except Exception as exc:
                logger.error("Failed to emit %s: %s", fmt, exc)
                return BuildResult(
                    success=False,
                    error=f"Emission failed for {fmt}: {exc}",
                    document_ir=document_ir,
                    token_payload=tokens,
                    timing_ms=(time.monotonic() - start) * 1000,
                )

        elapsed = round((time.monotonic() - start) * 1000, 1)

        provenance = BuildProvenance(
            org_id=config.org_id,
            timestamp=build_timestamp,
            token_source=config.token_source_label(),
            output_formats=list(config.output_formats),
            pipeline_version=_PIPELINE_VERSION,
            timing_ms=elapsed,
        )

        return BuildResult(
            success=True,
            artifacts=artifacts,
            validation_results=validation_results,
            document_ir=document_ir,
            token_payload=tokens,
            provenance=provenance,
            timing_ms=elapsed,
        )

    except Exception as exc:
        logger.error("Build pipeline failed: %s", exc, exc_info=True)
        return BuildResult(
            success=False,
            error=str(exc),
            timing_ms=(time.monotonic() - start) * 1000,
        )


# =============================================================================
# Batch Build
# =============================================================================


@dataclass
class BuildBatchConfig:
    """Configuration for building multiple brands in parallel."""

    brands: list[BuildConfig]
    max_concurrency: int = 4
    progress_callback: Any = None  # Callable[[str, BuildResult], None] | None


def build_batch(config: BuildBatchConfig) -> list[BuildResult]:
    """Build multiple brands with parallel execution.

    Args:
        config: Batch configuration with list of brand configs.

    Returns:
        List of BuildResult, one per brand, in the same order as input.
    """
    from concurrent.futures import ThreadPoolExecutor, as_completed

    results: dict[int, BuildResult] = {}

    def _build_one(idx: int, brand_config: BuildConfig) -> tuple[int, BuildResult]:
        result = build(brand_config)
        if config.progress_callback:
            config.progress_callback(brand_config.org_id, result)
        return idx, result

    with ThreadPoolExecutor(max_workers=config.max_concurrency) as pool:
        futures = {
            pool.submit(_build_one, i, bc): i for i, bc in enumerate(config.brands)
        }
        for future in as_completed(futures):
            idx, result = future.result()
            results[idx] = result

    return [results[i] for i in range(len(config.brands))]
