from __future__ import annotations

import json
import os
import re
from collections import Counter
from html import escape
from pathlib import Path
from typing import Any

try:
    from .runtime_paths import resolve_default_vault_path
    from .template_instances import (
        NETWORK_TEMPLATE_ID,
        _tokenize_match_text,
        build_template_plan as legacy_build_template_plan,
        detect_input_mode,
        normalize_csv_connections,
        normalize_paste_connections,
        render_template_instance as legacy_render_template_instance,
        summarize_connections,
    )
except ImportError:
    from runtime_paths import resolve_default_vault_path
    from template_instances import (
        NETWORK_TEMPLATE_ID,
        _tokenize_match_text,
        build_template_plan as legacy_build_template_plan,
        detect_input_mode,
        normalize_csv_connections,
        normalize_paste_connections,
        render_template_instance as legacy_render_template_instance,
        summarize_connections,
    )

_VAULT_TEMPLATE_ROOT = resolve_default_vault_path(__file__) / "templates"
_EACH_BLOCK_RE = re.compile(r"{{#each\s+([a-zA-Z0-9_.-]+)}}(.*?){{/each}}", re.DOTALL)
_INJECT_BLOCK_RE = re.compile(r"{{\s*inject:([a-zA-Z0-9_-]+)\s*}}")
_SLOT_RE = re.compile(r"{{\s*([a-zA-Z0-9_.-]+)(?:\s*\|\s*([a-zA-Z0-9_]+))?\s*}}")
INPUT_MODE_ALIASES = {
    "json": "json",
    "normalized_json": "json",
    "normalized-input": "json",
    "normalized_input": "json",
    "csv": "csv",
    "csv_upload": "csv",
    "paste": "paste",
    "pasted_structured_text": "paste",
    "chat": "chat",
    "chat_text": "chat",
    "freeform": "chat",
    "xlsx": "xlsx",
    "xls": "xlsx",
    "excel": "xlsx",
    "xlsx_upload": "xlsx",
}


def _title_case_slug(value: str) -> str:
    return " ".join(part.capitalize() for part in value.replace("_", "-").split("-") if part)


def _safe_json_load(path: Path) -> dict[str, Any]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError(f"{path} must contain a JSON object.")
    return payload


def _load_vault_templates() -> list[dict[str, Any]]:
    templates: list[dict[str, Any]] = []
    if not _VAULT_TEMPLATE_ROOT.exists():
        return templates

    for manifest_path in sorted(_VAULT_TEMPLATE_ROOT.glob("*/manifest.json")):
        manifest = _safe_json_load(manifest_path)
        directory = manifest_path.parent
        template_id = str(manifest.get("template_id") or directory.name).strip().lower()
        template_path = directory / "template.html"
        if not template_id or not template_path.exists():
            continue

        name = str(manifest.get("name") or _title_case_slug(template_id)).strip()
        tags = [str(tag).strip().lower() for tag in (manifest.get("tags") or []) if str(tag).strip()]
        categories = [str(item).strip().lower() for item in (manifest.get("categories") or []) if str(item).strip()]
        aliases = [str(item).strip().lower() for item in (manifest.get("match_aliases") or []) if str(item).strip()]
        compatible_with = [
            str(item).strip().lower()
            for item in (manifest.get("compatible_with") or [])
            if str(item).strip()
        ]
        supported_input_modes = [
            str(item).strip().lower()
            for item in (manifest.get("supported_input_modes") or [])
            if str(item).strip()
        ]
        payload = {
                "id": template_id,
                "template_id": template_id,
                "slug": str(manifest.get("slug") or template_id).strip().lower(),
                "title": name,
                "name": name,
                "subtitle": str(manifest.get("best_for") or "").strip() or None,
                "description": str(manifest.get("description") or "").strip(),
                "summary": str(manifest.get("best_for") or manifest.get("description") or "").strip(),
                "category": str(manifest.get("category") or (categories[0] if categories else "template")).strip().lower(),
                "categories": categories,
                "engine_id": str(manifest.get("engine_id") or "").strip(),
                "renderer_key": str(
                    manifest.get("renderer_key") or f"vault_template_{template_id.replace('-', '_')}"
                ).strip(),
                "input_schema_version": int(manifest.get("input_schema_version") or 1),
                "tags": tags,
                "best_for": str(manifest.get("best_for") or "").strip(),
                "complexity": str(manifest.get("complexity") or "low").strip().lower(),
                "composable": bool(manifest.get("composable")),
                "composition_role": str(manifest.get("composition_role") or "primary").strip().lower(),
                "compatible_with": compatible_with,
                "quality_baseline": int(manifest.get("quality_baseline") or 90),
                "created_at": str(manifest.get("created_at") or "").strip() or None,
                "created_by": str(manifest.get("created_by") or "hand-built").strip(),
                "source_artifact_id": manifest.get("source_artifact_id"),
                "match_aliases": aliases,
                "supported_input_modes": supported_input_modes,
                "default_display_name": str(manifest.get("default_display_name") or "Your").strip(),
                "slots": manifest.get("slots") if isinstance(manifest.get("slots"), dict) else {},
                "default_inject_at": str(manifest.get("default_inject_at") or "").strip().lower() or None,
                "normalizer": str(manifest.get("normalizer") or "").strip().lower() or None,
                "template_source": "vault",
                "template_dir": str(directory),
                "template_html_path": str(template_path),
                "example_data_path": str(directory / "example-data.json") if (directory / "example-data.json").exists() else None,
                "manifest": manifest,
            }
        payload["input_contract"] = _build_input_contract(payload, manifest)
        payload["supported_input_modes"] = payload["input_contract"]["supported_input_modes"]
        payload["input_guidance"] = build_template_input_guidance(payload)
        templates.append(payload)
    return templates


def _template_catalog() -> list[dict[str, Any]]:
    return sorted(_load_vault_templates(), key=lambda item: str(item.get("title") or item.get("id") or ""))


def _template_index() -> dict[str, dict[str, Any]]:
    index: dict[str, dict[str, Any]] = {}
    for template in _template_catalog():
        template_id = str(template.get("id") or "").strip().lower()
        if not template_id:
            continue
        index[template_id] = template
        slug = str(template.get("slug") or "").strip().lower()
        if slug:
            index.setdefault(slug, template)
    return index


def normalize_input_mode(value: str | None) -> str:
    normalized = str(value or "").strip().lower()
    if not normalized:
        return "json"
    return INPUT_MODE_ALIASES.get(normalized, normalized)


def _build_input_contract(template_payload: dict[str, Any], manifest: dict[str, Any]) -> dict[str, Any]:
    manifest_contract = manifest.get("input_contract") if isinstance(manifest.get("input_contract"), dict) else {}
    supported = manifest_contract.get("supported_input_modes")
    if not isinstance(supported, list) or not supported:
        supported = template_payload.get("supported_input_modes") or ["json"]
    normalized_supported: list[str] = []
    for item in supported:
        candidate = normalize_input_mode(item)
        if candidate and candidate not in normalized_supported:
            normalized_supported.append(candidate)
    if not normalized_supported:
        normalized_supported = ["json"]
    preferred = normalize_input_mode(
        manifest_contract.get("preferred_input_mode")
        or (normalized_supported[0] if normalized_supported else "json")
    )
    if preferred not in normalized_supported:
        normalized_supported.insert(0, preferred)
    return {
        "supported_input_modes": normalized_supported,
        "preferred_input_mode": preferred,
        "strict_input_mode": bool(manifest_contract.get("strict_input_mode")),
        "allow_chat_supplement": bool(manifest_contract.get("allow_chat_supplement")),
        "required_fields": [
            str(item).strip()
            for item in (manifest_contract.get("required_fields") or [])
            if str(item).strip()
        ],
        "required_columns": [
            str(item).strip()
            for item in (manifest_contract.get("required_columns") or [])
            if str(item).strip()
        ],
        "required_sheets": [
            str(item).strip()
            for item in (manifest_contract.get("required_sheets") or [])
            if str(item).strip()
        ],
        "rejection_help": str(manifest_contract.get("rejection_help") or "").strip() or None,
    }


def get_template_input_contract(template_or_id: str | dict[str, Any]) -> dict[str, Any]:
    template = (
        get_template_definition(template_or_id)
        if isinstance(template_or_id, str)
        else template_or_id
    )
    contract = template.get("input_contract") if isinstance(template.get("input_contract"), dict) else {}
    return dict(contract)


def build_template_input_guidance(template_or_id: str | dict[str, Any]) -> str:
    template = (
        get_template_definition(template_or_id)
        if isinstance(template_or_id, str)
        else template_or_id
    )
    contract = get_template_input_contract(template)
    supported = [str(item).strip() for item in (contract.get("supported_input_modes") or ["json"]) if str(item).strip()]
    preferred = str(contract.get("preferred_input_mode") or "").strip() or None
    parts = [f"Accepted input modes: {', '.join(supported)}."]
    if preferred:
        parts.append(f"Preferred mode: {preferred}.")
    if contract.get("strict_input_mode"):
        parts.append("This template is strict about how data is provided.")
    required_columns = [str(item).strip() for item in (contract.get("required_columns") or []) if str(item).strip()]
    if required_columns:
        parts.append(f"Required columns: {', '.join(required_columns)}.")
    required_sheets = [str(item).strip() for item in (contract.get("required_sheets") or []) if str(item).strip()]
    if required_sheets:
        parts.append(f"Required sheets: {', '.join(required_sheets)}.")
    rejection_help = str(contract.get("rejection_help") or "").strip()
    if rejection_help:
        parts.append(rejection_help)
    return " ".join(parts)


def _template_required_fields(template: dict[str, Any]) -> list[str]:
    contract = get_template_input_contract(template)
    required = [str(item).strip() for item in contract.get("required_fields") or [] if str(item).strip()]
    if not required:
        for key, slot in (template.get("slots") or {}).items():
            if isinstance(slot, dict) and slot.get("required"):
                required.append(str(key).strip())
    deduped: list[str] = []
    for item in required:
        if item and item not in deduped:
            deduped.append(item)
    return deduped


def _is_missing_contract_value(value: Any) -> bool:
    if value is None:
        return True
    if isinstance(value, str):
        return not value.strip()
    if isinstance(value, (list, tuple, set, dict)):
        return len(value) == 0
    return False


def _raise_input_contract_error(template: dict[str, Any], attempted_mode: str, *, reason: str | None = None) -> None:
    contract = get_template_input_contract(template)
    supported = contract.get("supported_input_modes") or ["json"]
    readable_supported = ", ".join(supported)
    help_text = contract.get("rejection_help")
    message = (
        f'Template "{template.get("title")}" does not accept input mode "{attempted_mode}". '
        f"Supported modes: {readable_supported}."
    )
    if reason:
        message += f" {reason}"
    preferred_mode = contract.get("preferred_input_mode")
    if preferred_mode and preferred_mode in supported and preferred_mode != attempted_mode:
        message += f" Preferred mode: {preferred_mode}."
    required_columns = contract.get("required_columns") or []
    if required_columns:
        message += f" Required columns: {', '.join(str(item) for item in required_columns)}."
    required_sheets = contract.get("required_sheets") or []
    if required_sheets:
        message += f" Required sheets: {', '.join(str(item) for item in required_sheets)}."
    if help_text:
        message += f" {help_text}"
    raise ValueError(message)


def validate_template_input_payload(
    *,
    template_id: str,
    normalized_data: dict[str, Any],
    input_mode: str | None = None,
) -> dict[str, Any]:
    template = get_template_definition(template_id)
    if not isinstance(normalized_data, dict):
        raise ValueError(f'Template "{template.get("title")}" expects normalized input to be a JSON object.')
    contract = get_template_input_contract(template)
    resolved_mode = normalize_input_mode(input_mode)
    supported = contract.get("supported_input_modes") or ["json"]
    if resolved_mode not in supported:
        _raise_input_contract_error(
            template,
            resolved_mode,
            reason="Use normalized_input JSON or switch to a supported upload/input path.",
        )

    required_fields = _template_required_fields(template)
    missing_fields = [
        field for field in required_fields if _is_missing_contract_value(normalized_data.get(field))
    ]
    if missing_fields:
        raise ValueError(
            f'Template "{template.get("title")}" is missing required fields: {", ".join(missing_fields)}.'
        )

    return {
        "input_mode": resolved_mode,
        "required_fields": required_fields,
        "missing_fields": missing_fields,
        "input_contract": contract,
    }


def list_public_templates() -> dict[str, Any]:
    templates = _template_catalog()
    return {
        "template_count": len(templates),
        "operator_guidance": (
            "Before generating from a template, tell the user which input modes are supported "
            "for that template and which mode is preferred, such as chat, paste, csv, xlsx, or json."
        ),
        "templates": templates,
    }


def get_template_definition(template_id: str) -> dict[str, Any]:
    normalized = (template_id or "").strip().lower()
    template = _template_index().get(normalized)
    if template:
        return template
    raise ValueError(f'Unknown template "{template_id}".')


def match_public_templates(prompt: str, top_k: int = 3) -> dict[str, Any]:
    raw_prompt = (prompt or "").strip()
    if not raw_prompt:
        raise ValueError("Add a prompt before matching templates.")

    normalized = raw_prompt.lower()
    prompt_tokens = _tokenize_match_text(raw_prompt)
    matches: list[dict[str, Any]] = []

    for template in _template_catalog():
        template_id = str(template.get("id") or "").lower()
        exact_targets = {
            template_id,
            str(template.get("slug") or "").lower(),
            str(template.get("title") or "").lower(),
            *(str(alias).lower() for alias in (template.get("match_aliases") or [])),
        }
        if normalized in exact_targets:
            matches.append(
                {
                    "template_id": template_id,
                    "name": template.get("title"),
                    "confidence": 1.0,
                    "match_method": "exact_name",
                    "engine_id": template.get("engine_id"),
                    "description": template.get("description"),
                    "composition_role": template.get("composition_role"),
                    "supported_input_modes": template.get("supported_input_modes"),
                    "preferred_input_mode": (template.get("input_contract") or {}).get("preferred_input_mode"),
                    "input_guidance": template.get("input_guidance"),
                }
            )
            continue

        tags = _tokenize_match_text(" ".join(template.get("tags") or []))
        category_tokens = _tokenize_match_text(" ".join(template.get("categories") or []))
        description_tokens = _tokenize_match_text(template.get("description"))
        best_for_tokens = _tokenize_match_text(template.get("best_for"))
        alias_tokens = _tokenize_match_text(" ".join(template.get("match_aliases") or []))
        slot_tokens = _tokenize_match_text(" ".join((template.get("slots") or {}).keys()))
        role = str(template.get("composition_role") or "primary").strip().lower()
        role_bonus = 4.0 if role == "primary" else 0.0
        weighted = (
            len(prompt_tokens & tags) * 3.0
            + len(prompt_tokens & category_tokens) * 2.0
            + len(prompt_tokens & best_for_tokens) * 2.0
            + len(prompt_tokens & alias_tokens) * 2.0
            + len(prompt_tokens & description_tokens)
            + len(prompt_tokens & slot_tokens)
            + role_bonus
        )
        if weighted <= 0:
            continue

        denominator = max(4.0, len(tags) + len(category_tokens) + len(alias_tokens) + len(slot_tokens) * 0.5)
        confidence = min(0.99, round(weighted / denominator, 3))
        matches.append(
            {
                "template_id": template_id,
                "name": template.get("title"),
                "confidence": confidence,
                "match_method": "tag_match",
                "engine_id": template.get("engine_id"),
                "description": template.get("description"),
                "composition_role": template.get("composition_role"),
                "supported_input_modes": template.get("supported_input_modes"),
                "preferred_input_mode": (template.get("input_contract") or {}).get("preferred_input_mode"),
                "input_guidance": template.get("input_guidance"),
            }
        )

    limit = max(1, int(top_k or 3))
    matches.sort(key=lambda item: (-float(item.get("confidence") or 0.0), str(item.get("template_id") or "")))
    return {
        "prompt": raw_prompt,
        "match_count": len(matches[:limit]),
        "operator_guidance": (
            "Before rendering, tell the user the matched template's supported_input_modes and preferred_input_mode "
            "so they can choose whether to continue with chat, paste, csv, xlsx, or json."
        ),
        "matches": matches[:limit],
    }


def _generic_template_summary(normalized_data: dict[str, Any]) -> dict[str, Any]:
    return {
        "provided_keys": sorted(str(key) for key in normalized_data.keys()),
        "slot_count": len(normalized_data),
    }


def normalize_template_input(
    *,
    template_id: str,
    raw_input: str,
    input_mode: str | None = None,
) -> dict[str, Any]:
    template = get_template_definition(template_id)
    cleaned = (raw_input or "").strip()
    if not cleaned:
        raise ValueError(f'Add input before rendering the "{template.get("title")}" template.')

    if template.get("normalizer") == "network_intelligence":
        declared_mode = normalize_input_mode(input_mode)
        resolved_mode = (
            declared_mode
            if declared_mode in {"csv", "paste", "json"}
            else normalize_input_mode(detect_input_mode(cleaned, input_mode))
        )
        supported = get_template_input_contract(template).get("supported_input_modes") or ["csv", "paste", "json"]
        if resolved_mode not in supported:
            _raise_input_contract_error(template, resolved_mode)
        if resolved_mode == "csv":
            connections, warnings, confidence = normalize_csv_connections(cleaned)
        elif resolved_mode == "json":
            try:
                parsed = json.loads(cleaned)
            except json.JSONDecodeError as exc:
                raise ValueError(
                    'Template "LinkedIn network intelligence" expects JSON raw_input to decode into an object with a "connections" array.'
                ) from exc
            if not isinstance(parsed, dict):
                raise ValueError(
                    'Template "LinkedIn network intelligence" expects JSON raw_input to decode into an object.'
                )
            raw_connections = parsed.get("connections")
            if not isinstance(raw_connections, list):
                raise ValueError(
                    'Template "LinkedIn network intelligence" expects JSON raw_input to include a "connections" array.'
                )
            connections = [item for item in raw_connections if isinstance(item, dict)]
            warnings = parsed.get("warnings") if isinstance(parsed.get("warnings"), list) else []
            confidence = float(parsed.get("parse_confidence") or 1.0)
        else:
            connections, warnings, confidence = normalize_paste_connections(cleaned)
        if len(connections) < 3:
            raise ValueError(f"IterFact only parsed {len(connections)} usable connections. Add a larger LinkedIn sample.")
        summary = (
            parsed.get("summary")
            if resolved_mode == "json" and isinstance(parsed.get("summary"), dict)
            else summarize_connections(connections)
        )
        validated = validate_template_input_payload(
            template_id=template_id,
            normalized_data={
                "connections": connections,
                "summary": summary,
            },
            input_mode=resolved_mode,
        )
        preview_rows = [
            {
                "name": connection["name"],
                "headline": connection["headline"],
                "industry": connection["industry"],
                "seniority": connection["seniority"],
                "connected_on": connection["connected_on"],
            }
            for connection in connections[:8]
        ]
        return {
            "template_id": template["id"],
            "template_title": template["title"],
            "input_mode": resolved_mode,
            "parse_confidence": confidence,
            "warning_count": len(warnings),
            "warnings": warnings,
            "summary": summary,
            "preview_rows": preview_rows,
            "input_contract": validated["input_contract"],
            "normalized_data": {
                "connections": connections,
                "summary": summary,
            },
        }

    resolved_mode = normalize_input_mode(input_mode)
    contract = get_template_input_contract(template)
    supported = contract.get("supported_input_modes") or ["json"]
    if resolved_mode not in supported:
        _raise_input_contract_error(template, resolved_mode)
    if resolved_mode == "xlsx":
        _raise_input_contract_error(
            template,
            resolved_mode,
            reason="Spreadsheet ingestion should happen through an upload-capable surface, or send normalized_input JSON directly.",
        )
    if resolved_mode == "csv":
        _raise_input_contract_error(
            template,
            resolved_mode,
            reason="CSV ingestion is not configured for this template's raw_input path yet. Send normalized_input JSON or use an upload-capable surface.",
        )
    if resolved_mode in {"chat", "paste"}:
        _raise_input_contract_error(
            template,
            resolved_mode,
            reason="Freeform chat/paste parsing is not enabled for this template. Use structured JSON or the recommended file upload path.",
        )
    try:
        parsed = json.loads(cleaned)
    except json.JSONDecodeError as exc:
        raise ValueError(
            f'Template "{template_id}" expects structured JSON for raw_input on this route. '
            "Freeform raw text parsing is not configured for this template."
        ) from exc

    if not isinstance(parsed, dict):
        raise ValueError(f'Template "{template_id}" raw_input JSON must decode to an object.')

    validated = validate_template_input_payload(
        template_id=template_id,
        normalized_data=parsed,
        input_mode=resolved_mode,
    )

    return {
        "template_id": template["id"],
        "template_title": template["title"],
        "input_mode": resolved_mode,
        "parse_confidence": 1.0,
        "warning_count": 0,
        "warnings": [],
        "summary": _generic_template_summary(parsed),
        "preview_rows": [],
        "input_contract": validated["input_contract"],
        "normalized_data": parsed,
    }


def _format_currency(value: Any) -> str:
    try:
        amount = float(value)
    except (TypeError, ValueError):
        return escape(str(value or ""))
    symbol = "-" if amount < 0 else ""
    amount = abs(amount)
    if amount >= 1_000_000:
        return f"{symbol}${amount / 1_000_000:.1f}M"
    return f"{symbol}${amount:,.0f}"


def _format_number(value: Any) -> str:
    try:
        amount = float(value)
    except (TypeError, ValueError):
        return escape(str(value or ""))
    if amount.is_integer():
        return f"{int(amount):,}"
    return f"{amount:,.1f}"


def _format_percent(value: Any) -> str:
    try:
        amount = float(value)
    except (TypeError, ValueError):
        return escape(str(value or ""))
    if amount <= 1:
        amount *= 100.0
    if abs(amount - round(amount)) < 0.05:
        return f"{round(amount):.0f}%"
    return f"{amount:.1f}%"


def _format_value_for_filter(value: Any, filter_name: str | None) -> str:
    name = (filter_name or "").strip().lower()
    if name == "format_currency":
        return _format_currency(value)
    if name == "format_number":
        return _format_number(value)
    if name == "format_percent":
        return _format_percent(value)
    if name == "json":
        return json.dumps(value, ensure_ascii=True)
    if value is None:
        return ""
    return escape(str(value))


def _resolve_data_path(data: dict[str, Any], path: str, loop_item: Any = None) -> Any:
    cleaned = (path or "").strip()
    if not cleaned:
        return None
    if cleaned == "this":
        return loop_item

    candidates: list[Any] = []
    if loop_item is not None:
        candidates.append(loop_item)
        if cleaned.startswith("this."):
            candidates = [loop_item]
            cleaned = cleaned[5:]
    candidates.append(data)

    for source in candidates:
        current: Any = source
        success = True
        for part in cleaned.split("."):
            if isinstance(current, dict) and part in current:
                current = current[part]
            else:
                success = False
                break
        if success:
            return current
    return None


def _render_template_markup(
    markup: str,
    data: dict[str, Any],
    *,
    loop_item: Any = None,
    injections: dict[str, str] | None = None,
) -> str:
    rendered = markup

    while True:
        match = _EACH_BLOCK_RE.search(rendered)
        if not match:
            break
        path = match.group(1)
        block = match.group(2)
        items = _resolve_data_path(data, path, loop_item=loop_item)
        if not isinstance(items, list):
            items = []
        replacement = "".join(
            _render_template_markup(block, data, loop_item=item, injections=injections)
            for item in items
        )
        rendered = rendered[: match.start()] + replacement + rendered[match.end() :]

    def replace_injection(match: re.Match[str]) -> str:
        key = str(match.group(1) or "").strip().lower()
        if not injections:
            return ""
        return injections.get(key, "")

    rendered = _INJECT_BLOCK_RE.sub(replace_injection, rendered)

    def replace_slot(match: re.Match[str]) -> str:
        path = str(match.group(1) or "").strip()
        filter_name = match.group(2)
        value = _resolve_data_path(data, path, loop_item=loop_item)
        return _format_value_for_filter(value, filter_name)

    return _SLOT_RE.sub(replace_slot, rendered)


def _extract_body_html(html: str) -> str:
    match = re.search(r"<body[^>]*>(.*)</body>", html, flags=re.IGNORECASE | re.DOTALL)
    if match:
        return match.group(1).strip()
    return html.strip()


def _wrap_panel_fragment(title: str, body_html: str) -> str:
    return f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{escape(title)}</title>
  <style>
    :root {{
      color-scheme: light dark;
      --color-background-primary: #ffffff;
      --color-background-secondary: #f8f6f0;
      --color-text-primary: #2c2c2a;
      --color-text-secondary: #5f5e5a;
      --color-border-secondary: rgba(44, 44, 42, 0.3);
      --color-border-tertiary: rgba(44, 44, 42, 0.15);
      --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      --border-radius-md: 8px;
      --border-radius-lg: 12px;
      --border-width-hairline: 0.5px;
      --ramp-teal-600: #0F6E56;
      --ramp-gray-50: #F1EFE8;
    }}
    @media (prefers-color-scheme: dark) {{
      :root {{
        --color-background-primary: #2c2c2a;
        --color-background-secondary: #242422;
        --color-text-primary: #f1efe8;
        --color-text-secondary: #d3d1c7;
        --color-border-secondary: rgba(241, 239, 232, 0.3);
        --color-border-tertiary: rgba(241, 239, 232, 0.15);
      }}
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      padding: 32px;
      background: var(--color-background-secondary);
      color: var(--color-text-primary);
      font-family: var(--font-sans);
      font-size: 15px;
      line-height: 1.6;
    }}
    .ifx-panel {{
      border: var(--border-width-hairline) solid var(--color-border-secondary);
      border-radius: var(--border-radius-lg);
      background: var(--color-background-primary);
      padding: 20px;
    }}
    .ifx-muted {{
      color: var(--color-text-secondary);
    }}
    .ifx-range {{
      width: 100%;
      accent-color: var(--ramp-teal-600);
    }}
    .ifx-list {{
      display: grid;
      gap: 12px;
      margin: 0;
      padding: 0;
      list-style: none;
    }}
    .ifx-bar {{
      height: 8px;
      border-radius: 999px;
      background: var(--ramp-gray-50);
      overflow: hidden;
      margin-top: 6px;
    }}
    .ifx-bar-fill {{
      height: 100%;
      border-radius: 999px;
      background: var(--ramp-teal-600);
    }}
  </style>
</head>
<body>{body_html}</body>
</html>"""


def _inject_panels(primary_html: str, panels: list[dict[str, Any]]) -> str:
    if not panels:
        return _INJECT_BLOCK_RE.sub("", primary_html)
    grouped: dict[str, list[str]] = {}
    for panel in panels:
        inject_at = str(panel.get("inject_at") or "").strip().lower()
        if not inject_at:
            continue
        grouped.setdefault(inject_at, []).append(str(panel.get("html") or ""))
    injections = {key: "\n".join(values) for key, values in grouped.items()}
    return _render_template_markup(primary_html, {}, injections=injections)


def _normalize_compose_items(compose: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
    items: list[dict[str, Any]] = []
    for item in compose or []:
        if not isinstance(item, dict):
            continue
        template_id = str(item.get("template_id") or item.get("templateId") or "").strip().lower()
        if not template_id:
            continue
        inject_at = str(item.get("inject_at") or item.get("injectAt") or "").strip().lower()
        items.append(
            {
                "template_id": template_id,
                "inject_at": inject_at,
                "data": item.get("data") if isinstance(item.get("data"), dict) else {},
            }
        )
    return items


def _validate_template_composition(primary_template: dict[str, Any], panels: list[dict[str, Any]]) -> list[dict[str, Any]]:
    compatible = {str(item).strip().lower() for item in (primary_template.get("compatible_with") or []) if str(item).strip()}
    validated: list[dict[str, Any]] = []
    for panel in panels:
        panel_template = get_template_definition(panel["template_id"])
        if compatible and panel["template_id"] not in compatible:
            raise ValueError(
                f'Template "{panel["template_id"]}" is not compatible with "{primary_template["id"]}".'
            )
        inject_at = panel.get("inject_at") or panel_template.get("default_inject_at")
        if not inject_at:
            raise ValueError(
                f'Template "{panel["template_id"]}" must declare inject_at or default_inject_at for composition.'
            )
        validated.append(
            {
                "template_id": panel_template["id"],
                "inject_at": str(inject_at).strip().lower(),
                "data": panel.get("data") if isinstance(panel.get("data"), dict) else {},
                "template": panel_template,
            }
        )
    return validated


def _network_recommendations(summary: dict[str, Any], owner_label: str) -> list[dict[str, str]]:
    top_industry = str(summary.get("top_industry") or "priority sector")
    c_suite_count = int(summary.get("c_suite_count") or 0)
    return [
        {
            "title": "Best fit outreach",
            "body": f"Start with {top_industry} because it already contains the densest share of {owner_label}'s visible network.",
        },
        {
            "title": "Required input",
            "body": "Upload a LinkedIn CSV export or paste a clean list of connections. The shell stays fixed while the data refreshes.",
        },
        {
            "title": "Preview next move",
            "body": f"Prioritize the {c_suite_count} founder or C-suite relationships first, then widen to adjacent operators in the same sector.",
        },
    ]


def _build_network_payload(
    template: dict[str, Any],
    normalized_data: dict[str, Any],
    *,
    display_name: str | None = None,
    network_label: str | None = None,
) -> dict[str, Any]:
    summary = normalized_data.get("summary") if isinstance(normalized_data.get("summary"), dict) else {}
    connections = normalized_data.get("connections") if isinstance(normalized_data.get("connections"), list) else []
    owner_label = (display_name or "").strip() or str(template.get("default_display_name") or "Your")
    network_name = (network_label or "").strip() or f"{owner_label} Network"
    connection_count = int(summary.get("connection_count") or len(connections))
    top_industry = str(summary.get("top_industry") or "General business")
    top_industry_count = int(summary.get("top_industry_count") or 0)
    industry_count = int(summary.get("industry_count") or 0)
    c_suite_count = int(summary.get("c_suite_count") or 0)
    top_industries = summary.get("top_industries") if isinstance(summary.get("top_industries"), list) else []
    top_seniority = summary.get("top_seniority") if isinstance(summary.get("top_seniority"), list) else []

    quarter_counts = Counter(
        str(connection.get("quarter") or "")
        for connection in connections
        if isinstance(connection, dict) and str(connection.get("quarter") or "").strip()
    )
    sorted_quarters = sorted(quarter_counts.items(), key=lambda item: item[0])[-6:]
    max_quarter = max((count for _, count in sorted_quarters), default=1)
    timeline_points = [
        {
            "label": quarter,
            "count_display": _format_number(count),
            "bar_width": max(12, round((count / max_quarter) * 100)),
        }
        for quarter, count in sorted_quarters
    ]

    industry_rows = []
    for item in top_industries[:5]:
        if not isinstance(item, dict):
            continue
        count = int(item.get("count") or 0)
        industry_rows.append(
            {
                "label": str(item.get("label") or "Unknown"),
                "count_display": _format_number(count),
                "share_display": _format_percent((count / max(connection_count, 1)) * 100.0),
                "bar_width": max(10, round((count / max(top_industry_count, 1)) * 100)),
            }
        )

    seniority_rows = []
    top_seniority_count = max((int(item.get("count") or 0) for item in top_seniority if isinstance(item, dict)), default=1)
    for item in top_seniority[:5]:
        if not isinstance(item, dict):
            continue
        count = int(item.get("count") or 0)
        seniority_rows.append(
            {
                "label": str(item.get("label") or "Unknown"),
                "count_display": _format_number(count),
                "share_display": _format_percent((count / max(connection_count, 1)) * 100.0),
                "bar_width": max(10, round((count / max(top_seniority_count, 1)) * 100)),
            }
        )

    return {
        "eyebrow": "Interactive documents",
        "title": f"{owner_label} LinkedIn network intelligence",
        "description": f"{network_name} mapped across industry concentration, seniority access, and connection velocity.",
        "connection_count_display": _format_number(connection_count),
        "industry_count_display": _format_number(industry_count),
        "c_suite_count_display": _format_number(c_suite_count),
        "top_industry_label": top_industry,
        "top_industry_count_display": _format_number(top_industry_count),
        "top_industry_share_display": _format_percent((top_industry_count / max(connection_count, 1)) * 100.0),
        "latest_connected_on": str(summary.get("latest_connected_on") or "recent quarters"),
        "quarter_count_display": _format_number(summary.get("quarter_count") or len(sorted_quarters)),
        "industries": industry_rows,
        "seniority_rows": seniority_rows,
        "timeline_points": timeline_points,
        "recommendations": _network_recommendations(summary, owner_label),
        "best_fit_copy": "Best fit for LinkedIn exports, relationship mapping, and fast account-priority reviews.",
        "required_input_copy": "Required input: LinkedIn CSV or pasted connection text. Preview updates without changing the shell.",
        "preview_copy": "Preview surfaces the same consistent sections every time so comparisons stay stable artifact to artifact.",
    }


def _normalize_kpi_cards(cards: Any) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for item in cards or []:
        if not isinstance(item, dict):
            continue
        rows.append(
            {
                "label": str(item.get("label") or "Metric"),
                "value_display": str(item.get("value") or item.get("value_display") or "—"),
                "trend": str(item.get("trend") or "flat"),
            }
        )
    return rows


def _normalize_chart_bars(chart_data: Any) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    values = [float(item.get("value") or 0) for item in chart_data or [] if isinstance(item, dict)]
    max_value = max(values, default=1.0)
    for item in chart_data or []:
        if not isinstance(item, dict):
            continue
        value = float(item.get("value") or 0)
        rows.append(
            {
                "label": str(item.get("label") or "Period"),
                "value_display": _format_currency(value),
                "bar_height": max(16, round((value / max_value) * 100)),
            }
        )
    return rows


def _build_revenue_payload(template: dict[str, Any], normalized_data: dict[str, Any]) -> dict[str, Any]:
    headline = normalized_data.get("headline_metric") if isinstance(normalized_data.get("headline_metric"), dict) else {}
    value = headline.get("value")
    format_name = str(headline.get("format") or "currency")
    if format_name == "percent":
        value_display = _format_percent(value)
    elif format_name == "number":
        value_display = _format_number(value)
    else:
        value_display = _format_currency(value)
    return {
        "title": str(normalized_data.get("title") or template.get("title") or "Revenue dashboard"),
        "subtitle": str(
            normalized_data.get("subtitle")
            or "Best fit for board updates, executive reviews, and recurring revenue reporting."
        ),
        "headline_label": str(headline.get("label") or "ARR"),
        "headline_value": value_display,
        "headline_delta": str(headline.get("delta") or "Provide a supported comparison before adding delta language."),
        "kpi_cards": _normalize_kpi_cards(normalized_data.get("kpi_cards")),
        "chart_bars": _normalize_chart_bars(normalized_data.get("chart_data")),
        "narrative_points": [
            {"text": point}
            for point in (
                normalized_data.get("narrative_points")
                if isinstance(normalized_data.get("narrative_points"), list)
                else [
                    "Best fit: executive reviews that need a clear headline metric first.",
                    "Required input: one primary KPI, optional secondary cards, and a short chart series.",
                    "Preview: panels can be composed into the sidebar or bottom rail without rewriting the primary shell.",
                ]
            )
        ],
        "footer_text": str(normalized_data.get("footer_text") or "Generated by IterFact templates"),
    }


def _build_segment_panel_payload(normalized_data: dict[str, Any]) -> dict[str, Any]:
    segments = normalized_data.get("segments") if isinstance(normalized_data.get("segments"), list) else []
    max_percent = max((float(item.get("percent") or 0) for item in segments if isinstance(item, dict)), default=100.0)
    rows = []
    for item in segments:
        if not isinstance(item, dict):
            continue
        percent = float(item.get("percent") or 0)
        rows.append(
            {
                "name": str(item.get("name") or "Segment"),
                "percent_display": _format_percent(percent),
                "bar_width": max(8, round((percent / max(max_percent, 1.0)) * 100)),
            }
        )
    return {
        "panel_title": str(normalized_data.get("panel_title") or "Segment breakdown"),
        "panel_copy": str(
            normalized_data.get("panel_copy")
            or "Best fit sidebar for segment concentration, coverage gaps, and prioritization."
        ),
        "segments": rows,
    }


def _build_scenario_panel_payload(normalized_data: dict[str, Any]) -> dict[str, Any]:
    default_percent = int(normalized_data.get("default_percent") or 65)
    options = normalized_data.get("options") if isinstance(normalized_data.get("options"), list) else []
    option_rows = [{"label": str(item)} for item in options if str(item).strip()]
    safe_percent = max(0, min(default_percent, 100))
    return {
        "panel_title": str(normalized_data.get("panel_title") or "Scenario slider"),
        "panel_copy": str(
            normalized_data.get("panel_copy")
            or "Required input: a default range position plus named scenarios. Preview keeps the interaction lightweight."
        ),
        "default_percent": safe_percent,
        "default_percent_display": f"{safe_percent}%",
        "options": option_rows,
    }


def _build_template_payload(
    template: dict[str, Any],
    normalized_data: dict[str, Any],
    *,
    display_name: str | None = None,
    network_label: str | None = None,
) -> dict[str, Any]:
    template_id = str(template.get("id") or "").strip().lower()
    if template_id == NETWORK_TEMPLATE_ID:
        return _build_network_payload(
            template,
            normalized_data,
            display_name=display_name,
            network_label=network_label,
        )
    if template_id == "revenue-dashboard-dark":
        return _build_revenue_payload(template, normalized_data)
    if template_id == "segment-breakdown-sidebar":
        return _build_segment_panel_payload(normalized_data)
    if template_id == "scenario-slider-panel":
        return _build_scenario_panel_payload(normalized_data)
    return dict(normalized_data)


def _compose_rendered_html(primary_template: dict[str, Any], primary_html: str, compose: list[dict[str, Any]]) -> str:
    if not compose:
        return _INJECT_BLOCK_RE.sub("", primary_html)
    validated_panels = _validate_template_composition(primary_template, _normalize_compose_items(compose))
    panel_fragments: list[dict[str, Any]] = []
    for panel in validated_panels:
        panel_template = panel["template"]
        panel_markup = Path(str(panel_template["template_html_path"])).read_text(encoding="utf-8")
        panel_payload = _build_template_payload(panel_template, panel["data"])
        panel_html = _render_template_markup(panel_markup, panel_payload)
        panel_fragments.append({"inject_at": panel["inject_at"], "html": _extract_body_html(panel_html)})
    return _inject_panels(primary_html, panel_fragments)


def _build_template_narrative_spine(
    *,
    core_thesis: str,
    audience: str,
    desired_action: str,
    throughlines: list[str],
    must_keep_facts: list[str],
    key_entities: list[str],
    closing_obligation: str,
    section_jobs: list[str],
    claim_map: list[dict[str, Any]],
) -> dict[str, Any]:
    return {
        "source": "planner",
        "core_thesis": core_thesis,
        "audience": audience,
        "desired_action": desired_action,
        "throughlines": throughlines,
        "must_keep_facts": must_keep_facts,
        "must_not_claim": [
            "Avoid unsupported comparisons.",
            "Avoid overstatement beyond the supplied evidence.",
        ],
        "key_entities": key_entities,
        "claim_map": claim_map,
        "closing_obligation": closing_obligation,
        "section_jobs": section_jobs,
    }


def build_template_plan(
    *,
    template_id: str,
    normalized_data: dict[str, Any],
    display_name: str | None = None,
    network_label: str | None = None,
    compose: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    template = get_template_definition(template_id)
    template_id_normalized = str(template.get("id") or "").strip().lower()

    if template_id_normalized == NETWORK_TEMPLATE_ID:
        summary = normalized_data.get("summary") if isinstance(normalized_data.get("summary"), dict) else {}
        owner_label = (display_name or "").strip() or str(template.get("default_display_name") or "Your")
        network_name = (network_label or "").strip() or f"{owner_label} Network"
        connection_count = int(summary.get("connection_count") or 0)
        top_industry = str(summary.get("top_industry") or "priority sector")
        c_suite_count = int(summary.get("c_suite_count") or 0)
        return {
            "wrapper": {"engine": None},
            "theme": "paper-emerald",
            "meta": {
                "title": f"{owner_label} LinkedIn network intelligence",
                "description": f"{network_name} analyzed across industry concentration, seniority access, and connection velocity.",
                "document_format": "template instance",
                "template_match": "vault_template_network_intelligence",
                "narrative_spine": _build_template_narrative_spine(
                    core_thesis=f"{network_name} already shows visible concentration in a few high-signal relationship pockets.",
                    audience="Operators, founders, and business-development leaders reviewing a LinkedIn network export.",
                    desired_action="Prioritize outreach around the densest industry and seniority clusters first.",
                    throughlines=[
                        "Concentration matters more than raw count.",
                        "Seniority access shapes follow-up quality.",
                        "Velocity reveals where the network is actively growing.",
                    ],
                    must_keep_facts=[
                        f"Connection count: {connection_count}",
                        f"Top industry: {top_industry}",
                        f"C-suite count: {c_suite_count}",
                    ],
                    key_entities=[network_name, top_industry, "LinkedIn network intelligence"],
                    closing_obligation="Close with clear next-step guidance tied to the densest relationship pockets and the required input to refresh the template.",
                    section_jobs=[
                        "Quantify the current network coverage signal.",
                        "Show where industry concentration is strongest.",
                        "Show which seniority bands carry the highest leverage.",
                        "Show how connection velocity evolves over time.",
                        "Translate the snapshot into best-fit next moves.",
                        "Explain the required input and preview behavior for repeat use.",
                    ],
                    claim_map=[
                        {"claim": f"{network_name} is concentrated enough to prioritize sectors instead of treating the network as flat.", "support_sections": [1, 2]},
                        {"claim": "Seniority mix affects which relationships deserve immediate follow-up.", "support_sections": [3, 5]},
                        {"claim": "The template is reusable because the required input and preview shell are fixed.", "support_sections": [4, 6]},
                    ],
                ),
            },
            "sections": [
                {"title": "Network coverage signal", "components": [{"engine": "03", "instance_id": "coverage-kpis"}]},
                {"title": "Industry concentration", "components": [{"engine": "07", "instance_id": "industry-bars"}]},
                {"title": "Seniority access", "components": [{"engine": "03", "instance_id": "seniority-bands"}]},
                {"title": "Connection velocity preview", "components": [{"engine": "03", "instance_id": "velocity-timeline"}]},
                {"title": "Best fit next moves", "components": [{"engine": "12", "instance_id": "strategic-guidance"}]},
                {"title": "Required input and template preview", "components": [{"engine": "12", "instance_id": "productization-cues"}]},
            ],
            "template_instance": {
                "template_id": template_id,
                "renderer_key": template["renderer_key"],
                "composition": [],
            },
        }

    if template_id_normalized == "revenue-dashboard-dark":
        headline = normalized_data.get("headline_metric") if isinstance(normalized_data.get("headline_metric"), dict) else {}
        headline_label = str(headline.get("label") or "ARR")
        panel_ids = [str(item.get("template_id") or "").strip().lower() for item in _normalize_compose_items(compose)]
        sidebar_included = "segment-breakdown-sidebar" in panel_ids
        scenario_included = "scenario-slider-panel" in panel_ids
        sections = [
            {"title": "Headline revenue signal", "components": [{"engine": "03", "instance_id": "headline-dashboard"}]},
            {"title": "KPI detail and support", "components": [{"engine": "03", "instance_id": "kpi-support"}]},
            {"title": "Best fit, input, and preview", "components": [{"engine": "12", "instance_id": "productization-copy"}]},
        ]
        section_jobs = [
            "Lead with the primary KPI and its supported comparison context.",
            "Show secondary KPI context without overwhelming the main signal.",
            "Explain best fit, required input, and preview behavior for repeat use.",
        ]
        if sidebar_included:
            sections.insert(2, {"title": "Segment concentration sidebar", "components": [{"engine": "07", "instance_id": "segment-panel"}]})
            section_jobs.insert(2, "Use the sidebar panel to show segment concentration without interrupting the dashboard rhythm.")
        if scenario_included:
            sections.append({"title": "Scenario preview", "components": [{"engine": "17", "instance_id": "scenario-panel"}]})
            section_jobs.append("Close with an adjustable scenario panel that keeps the interaction lightweight and inspectable.")

        return {
            "wrapper": {"engine": None},
            "theme": "obsidian-cyan",
            "meta": {
                "title": str(normalized_data.get("title") or template.get("title") or "Revenue dashboard"),
                "description": str(template.get("description") or "Composed revenue dashboard template."),
                "document_format": "template instance",
                "template_match": "vault_template_revenue_dashboard_dark",
                "narrative_spine": _build_template_narrative_spine(
                    core_thesis="The primary KPI should lead, with supporting cards and composed panels clarifying what the reader should trust or inspect next.",
                    audience="Operators, executives, and buyers reviewing a revenue dashboard quickly.",
                    desired_action="Understand the main KPI, inspect segment or scenario detail, and decide whether the template fits the reporting job.",
                    throughlines=[
                        "Lead with the single number that matters most.",
                        "Support the headline with only the panels that add explanatory value.",
                        "Keep required input and preview behavior explicit for self-serve use.",
                    ],
                    must_keep_facts=[headline_label],
                    key_entities=[str(normalized_data.get("title") or template.get("title") or "Revenue dashboard"), headline_label],
                    closing_obligation="Close with clear best-fit guidance and next input steps so the artifact reads as reusable product, not one-off design.",
                    section_jobs=section_jobs,
                    claim_map=[
                        {"claim": "The dashboard should make the headline KPI understandable in one screen.", "support_sections": [1, 2]},
                        {"claim": "Composed panels should only appear when they sharpen the story rather than decorate it.", "support_sections": [index + 1 for index, section in enumerate(sections) if "panel" in str(section.get("title") or "").lower()] or [2]},
                        {"claim": "The template is reusable because best fit, required input, and preview behavior are explicit.", "support_sections": [len(sections)]},
                    ],
                ),
            },
            "sections": sections,
            "template_instance": {
                "template_id": template_id,
                "renderer_key": template["renderer_key"],
                "composition": _normalize_compose_items(compose),
            },
        }

    generic_title = str(normalized_data.get("title") or template.get("title") or _title_case_slug(template_id_normalized))
    return {
        "wrapper": {"engine": None},
        "theme": "paper-emerald",
        "meta": {
            "title": generic_title,
            "description": str(template.get("description") or "Template instance render."),
            "document_format": "template instance",
            "template_match": f"vault_template_{template_id_normalized.replace('-', '_')}",
            "narrative_spine": _build_template_narrative_spine(
                core_thesis=f"{generic_title} should make its main structure understandable without redesigning the shell.",
                audience="Operators reviewing a structured IterFact template.",
                desired_action="Confirm fit, required input, and preview behavior before reuse.",
                throughlines=["The shell stays fixed.", "The data changes cleanly inside known slots."],
                must_keep_facts=[generic_title],
                key_entities=[generic_title],
                closing_obligation="Close by confirming fit, required input, and preview behavior.",
                section_jobs=[
                    "Introduce the template and its main purpose.",
                    "Explain fit and required input.",
                    "Explain preview and reuse behavior.",
                ],
                claim_map=[
                    {"claim": "The template should be reusable without rewriting the layout.", "support_sections": [1, 2, 3]},
                    {"claim": "The operator should understand what input is required before using it.", "support_sections": [2, 3]},
                ],
            ),
        },
        "sections": [
            {"title": "Template purpose", "components": [{"engine": "12", "instance_id": "template-purpose"}]},
            {"title": "Best fit and required input", "components": [{"engine": "12", "instance_id": "template-inputs"}]},
            {"title": "Preview and reuse", "components": [{"engine": "12", "instance_id": "template-preview"}]},
        ],
        "template_instance": {
            "template_id": template_id,
            "renderer_key": template["renderer_key"],
            "composition": _normalize_compose_items(compose),
        },
    }


def render_template_instance(
    *,
    template_id: str,
    normalized_data: dict[str, Any],
    display_name: str | None = None,
    network_label: str | None = None,
    compose: list[dict[str, Any]] | None = None,
) -> str:
    template = get_template_definition(template_id)
    markup = Path(str(template["template_html_path"])).read_text(encoding="utf-8")
    payload = _build_template_payload(
        template,
        normalized_data,
        display_name=display_name,
        network_label=network_label,
    )
    rendered = _render_template_markup(markup, payload)
    rendered = _compose_rendered_html(template, rendered, compose or [])
    if str(template.get("composition_role") or "primary").strip().lower() != "primary" and "<html" not in rendered.lower():
        return _wrap_panel_fragment(str(payload.get("panel_title") or payload.get("title") or template.get("title")), rendered)
    return rendered
