"""MCP tool for generating normalized AI-readable service maps.

Composes a single deterministic artifact from BPA export data, reusing
existing normalization from service_insights and _form_shared.

NOTE: This module imports private functions from service_insights.py:
  _build_branch_map, _build_form_lookup, _unwrap_export
If those functions are refactored, tests in test_service_map.py will catch breakage.
"""

from __future__ import annotations

import json
import logging
from collections import defaultdict
from typing import Any

from fastmcp.exceptions import ToolError

from mcp_eregistrations_bpa.bpa_client import BPAClient
from mcp_eregistrations_bpa.bpa_client.errors import (
    BPAClientError,
    BPANotFoundError,
    translate_error,
)
from mcp_eregistrations_bpa.tools._form_shared import (
    parse_form_schema,
    simplify_components,
)
from mcp_eregistrations_bpa.tools.formio_helpers import get_all_component_keys
from mcp_eregistrations_bpa.tools.large_response import large_response_handler
from mcp_eregistrations_bpa.tools.service_insights import (
    _STATUS_TYPE_TO_EDGE,
    _build_branch_map,
    _build_form_lookup,
    _unwrap_export,
)

logger = logging.getLogger(__name__)

__all__ = [
    "service_ai_map",
    "register_service_map_tools",
]

# Export options: union of workflow_graph + service_branches needs
_AI_MAP_EXPORT_OPTIONS = {
    "serviceSelected": True,
    "rolesSelected": True,
    "determinantsSelected": True,
    "guideFormSelected": True,
    "applicantFormSelected": True,
    "sendFileFormSelected": True,
    "paymentFormSelected": True,
    "registrationsSelected": True,
    "catalogsSelected": False,
    "botsSelected": False,
    "printDocumentsSelected": False,
    "costsSelected": False,
    "requirementsSelected": False,
    "resultsSelected": False,
    "activityConditionsSelected": False,
    "registrationLawsSelected": False,
    "serviceLocationsSelected": False,
    "serviceTutorialsSelected": False,
    "serviceTranslationsSelected": False,
    "copyService": False,
}


_AGENT_TYPE_MAP = {
    "BotRole": "bot",
    "UserRole": "human",
}


def _build_intended_flow(
    roles: list[dict[str, Any]],
    warnings: list[str],
) -> dict[str, Any]:
    """Build the intended_flow section from export roles data.

    Extracts typed edges from role statuses, then detects parallel groups
    (fan-out), convergence roles (fan-in), and correction paths.
    """
    if not roles:
        warnings.append("Service has no roles configured")
        return {
            "roles": [],
            "edges": [],
            "start_roles": [],
            "parallel_groups": [],
            "convergence_roles": [],
            "correction_paths": [],
        }

    # Build edges first so we can compute used_in_flow from the graph
    edges: list[dict[str, Any]] = []
    forward_graph: dict[str, list[str]] = defaultdict(list)
    inbound_forward: dict[str, list[str]] = defaultdict(list)
    all_edge_participants: set[str] = set()

    for role in roles:
        role_name = role.get("name", "")
        for status in role.get("statuses") or []:
            status_type = status.get("roleStatusType")
            edge_type = _STATUS_TYPE_TO_EDGE.get(status_type)
            if not edge_type:
                continue
            for dest in status.get("destinations") or []:
                edges.append(
                    {
                        "from": role_name,
                        "to": dest,
                        "transition_type": edge_type,
                        "status_name": status.get("name"),
                    }
                )
                all_edge_participants.add(role_name)
                all_edge_participants.add(dest)
                if edge_type == "forward":
                    forward_graph[role_name].append(dest)
                    inbound_forward[dest].append(role_name)

    # Normalize role entries using export field names
    # Export keys: name, shortName, type, startRole, businessKey,
    #   visibleForApplicant, assignedTo, institutions[], statuses[]
    # NOT present: id, sortOrderNumber, usedInFlow
    normalized_roles = []
    for idx, role in enumerate(roles):
        role_type = role.get("type", "UserRole")
        role_name = role.get("name", "")

        # Extract institution/unit from first institution entry
        institutions = role.get("institutions") or []
        first_inst = institutions[0] if institutions else {}

        # Normalize statuses for this role
        role_statuses = []
        for status in role.get("statuses") or []:
            edge_type = _STATUS_TYPE_TO_EDGE.get(
                status.get("roleStatusType"), "unknown"
            )
            role_statuses.append(
                {
                    "name": status.get("name", ""),
                    "type": edge_type,
                    "active": status.get("active", True),
                    "destinations": status.get("destinations") or [],
                }
            )

        normalized_roles.append(
            {
                "business_key": role.get("businessKey", ""),
                "name": role_name,
                "short_name": role.get("shortName"),
                "sort_order": idx,
                "role_type": role_type,
                "start_role": role.get("startRole", False),
                "visible_for_applicant": role.get("visibleForApplicant", False),
                "assigned_to": role.get("assignedTo"),
                "used_in_flow": role_name in all_edge_participants,
                "agent_type": _AGENT_TYPE_MAP.get(role_type, "unknown"),
                "institution": first_inst.get("institutionName"),
                "unit": first_inst.get("unitName"),
                "statuses": role_statuses,
            }
        )

    start_roles = [r.get("name", "") for r in roles if r.get("startRole")]

    parallel_groups: list[list[str]] = []
    seen_groups: set[frozenset[str]] = set()
    for _role_name, dests in forward_graph.items():
        if len(dests) > 1:
            group = frozenset(dests)
            if group not in seen_groups:
                seen_groups.add(group)
                parallel_groups.append(sorted(dests))

    convergence_roles = sorted(
        name for name, preds in inbound_forward.items() if len(preds) > 1
    )

    correction_paths = [
        {"from": e["from"], "to": e["to"], "status_name": e["status_name"]}
        for e in edges
        if e["transition_type"] == "corrections"
    ]

    return {
        "roles": normalized_roles,
        "edges": edges,
        "start_roles": start_roles,
        "parallel_groups": parallel_groups,
        "convergence_roles": convergence_roles,
        "correction_paths": correction_paths,
    }


def _build_service_section(
    export_data: dict[str, Any],
    service_data: dict[str, Any],
    instance: str | None,
    warnings: list[str],
) -> dict[str, Any]:
    """Build the service metadata section."""
    updated_at = service_data.get("updatedAt")
    warnings.append(
        "BPA has no distinct publish timestamp; published_at falls back to updated_at"
    )
    return {
        "instance": instance,
        "service_id": str(service_data.get("id", export_data.get("id", ""))),
        "name": service_data.get("name", export_data.get("name", "")),
        "short_name": service_data.get("shortName", export_data.get("shortName")),
        "active": service_data.get("status") == "ACTIVE",
        "source_version": {
            "updated_at": updated_at,
            "published_at": updated_at,
        },
    }


def _build_visibility_map(
    behaviours: list[dict[str, Any]],
    determinants: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
    """Build componentKey → [visibility_rule] map from behaviours + determinants.

    Inverts the controller-oriented branch_fields into a target-oriented view:
    for each component, which determinants control its visibility and how.
    """
    # determinant oldId → name lookup
    det_names: dict[str, str] = {}
    for d in determinants:
        old_id = d.get("oldId")
        if old_id:
            det_names[old_id] = d.get("name", "")

    rules: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for behaviour in behaviours:
        comp_key = behaviour.get("componentKey", "")
        if not comp_key:
            continue
        for effect in behaviour.get("effects") or []:
            jd_str = effect.get("jsonDeterminants")
            if not jd_str or jd_str in ("null", "[]"):
                continue
            try:
                parsed = json.loads(jd_str) if isinstance(jd_str, str) else jd_str
            except (json.JSONDecodeError, TypeError):
                continue

            # Collect determinant references
            when: list[str] = []
            for group in parsed:
                for item in group.get("items") or []:
                    det_id = item.get("determinantId")
                    if det_id:
                        when.append(det_names.get(det_id, det_id))

            # Collect property effects (activate/show/disabled)
            props: dict[str, str] = {}
            for pe in effect.get("propertyEffects") or []:
                name = pe.get("name")
                value = pe.get("value")
                if name:
                    props[name] = value

            if when:
                rules[comp_key].append({"when": when, **props})

    return dict(rules)


def _iter_all_components(
    components: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    """Recursively collect all components from a FormIO tree."""
    result: list[dict[str, Any]] = []
    for comp in components:
        if not isinstance(comp, dict):
            continue
        result.append(comp)
        for child_key in ("components",):
            children = comp.get(child_key)
            if isinstance(children, list):
                result.extend(_iter_all_components(children))
        columns = comp.get("columns")
        if isinstance(columns, list):
            for col in columns:
                if isinstance(col, dict):
                    col_comps = col.get("components", [])
                    if isinstance(col_comps, list):
                        result.extend(_iter_all_components(col_comps))
    return result


def _build_forms_section(
    data: dict[str, Any],
    include_role_forms: bool,
    warnings: list[str],
) -> dict[str, Any]:
    """Build the forms section from export data."""
    forms: dict[str, Any] = {}

    # --- Applicant form ---
    applicant_page = data.get("applicantFormPage")
    if not isinstance(applicant_page, dict):
        applicant_page = {}

    schema = applicant_page.get("formSchema", {})
    if isinstance(schema, str):
        try:
            schema = json.loads(schema)
        except (json.JSONDecodeError, TypeError):
            schema = {}
    if not isinstance(schema, dict):
        schema = {}

    components = schema.get("components", [])
    if not isinstance(components, list):
        components = []

    all_keys = get_all_component_keys(components) if components else set()
    simplified = simplify_components(components) if components else []

    # Branch analysis using existing helpers
    form_lookup = _build_form_lookup(data)
    determinants = data.get("serviceDeterminants", [])
    behaviours = data.get("componentBehaviours", [])
    branch_result = _build_branch_map(determinants, behaviours, form_lookup)

    # Build per-field visibility rules from behaviours + determinants
    visibility_map = _build_visibility_map(behaviours, determinants)
    for field in simplified:
        key = field.get("key")
        if key and key in visibility_map:
            field["visibility_rules"] = visibility_map[key]

    # Warn about classification-backed fields that couldn't resolve values
    classification_fields = [
        comp.get("key")
        for comp in _iter_all_components(components)
        if comp.get("type") == "select" and comp.get("data", {}).get("dataSrc") == "url"
    ]
    if classification_fields:
        warnings.append(
            f"{len(classification_fields)} classification-backed field(s) could not "
            f"resolve values: {classification_fields}"
        )

    # Warn about components with custom conditional logic
    custom_conditional_keys = [
        comp.get("key")
        for comp in _iter_all_components(components)
        if comp.get("customConditional") or comp.get("logic")
    ]
    if custom_conditional_keys:
        warnings.append(
            f"{len(custom_conditional_keys)} component(s) have custom conditional "
            f"logic not captured in branch_fields: {custom_conditional_keys}"
        )

    # Systematic warnings for contract field coverage
    unresolved_count = sum(
        1 for f in simplified if f.get("value_source") == "unresolved"
    )
    if unresolved_count:
        warnings.append(
            f"{unresolved_count} selectable field(s) have value_source='unresolved' "
            "(values populated at runtime, not in export)"
        )

    forms["applicant"] = {
        "field_count": len(all_keys),
        "branch_fields": branch_result.get("branches", []),
        "fields": simplified,
    }

    # --- Role forms (opt-in) ---
    if include_role_forms:
        roles = data.get("roles", [])
        role_forms: list[dict[str, Any]] = []
        for role in roles:
            role_entry: dict[str, Any] = {
                "role_id": str(role.get("id", "")),
                "role_name": role.get("name", ""),
            }
            role_schema_raw = role.get("formSchema")
            if role_schema_raw:
                role_form_data = {"formSchema": role_schema_raw}
                parsed = parse_form_schema(role_form_data)
                role_components = parsed.get("components", [])
                if isinstance(role_components, list) and role_components:
                    role_keys = get_all_component_keys(role_components)
                    role_entry["field_count"] = len(role_keys)
                    role_entry["fields"] = simplify_components(role_components)
                else:
                    role_entry["field_count"] = 0
                    role_entry["fields"] = []
            else:
                role_entry["field_count"] = None
                role_entry["fields"] = []
                role_entry["availability"] = "no_schema"
                warnings.append(f"Role '{role.get('name')}' has no form schema")
            role_forms.append(role_entry)
        forms["roles"] = role_forms

    return forms


async def _build_documents_section(
    client: Any,
    registrations: list[dict[str, Any]],
    warnings: list[str],
) -> dict[str, Any]:
    """Build documents section by traversing registrations."""
    all_docs: list[dict[str, Any]] = []

    for reg in registrations:
        reg_id = reg.get("id")
        reg_name = reg.get("name", "")
        if not reg_id:
            continue
        try:
            doc_reqs = await client.get_list(
                "/registration/{registration_id}/document_requirement",
                path_params={"registration_id": reg_id},
                resource_type="document_requirement",
            )
            for doc in doc_reqs:
                all_docs.append(
                    {
                        "requirement_id": str(doc.get("id", "")),
                        "name": doc.get("name", ""),
                        "required": bool(doc.get("required", False)),
                        "registration_id": str(reg_id),
                        "registration_name": reg_name,
                    }
                )
        except BPAClientError as e:
            warnings.append(
                f"Failed to fetch documents for registration '{reg_name}': {e}"
            )

    return {"applicant_documents": all_docs}


@large_response_handler(
    threshold_bytes=100 * 1024,  # 100KB threshold
    navigation={
        "flow": "jq '.intended_flow'",
        "applicant_form": "jq '.forms.applicant'",
        "branch_fields": "jq '.forms.applicant.branch_fields'",
        "documents": "jq '.documents'",
        "warnings": "jq '.warnings'",
    },
)
async def service_ai_map(
    service_id: str | int,
    include_role_forms: bool = False,
    include_documents: bool = True,
    include_raw_sources: bool = False,
    instance: str | None = None,
) -> dict[str, Any]:
    """Return a normalized AI-readable service map. Read-only.

    Composes workflow graph, form schemas, branch analysis, and document
    requirements into one deterministic artifact.

    Args:
        service_id: BPA service UUID.
        include_role_forms: Include per-role form schemas (default: False).
        include_documents: Include document requirements (default: True).
        include_raw_sources: Include raw export data (default: False).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with service, intended_flow, forms, documents, warnings, sources.
    """
    if not service_id:
        raise ToolError(
            "'service_id' is required. Use 'service_list' to find valid service IDs."
        )

    warnings: list[str] = []
    warnings.append(
        "Camunda activityId not available from BPA; "
        "use DS service_roles for Camunda IDs"
    )

    try:
        async with BPAClient.for_instance(instance) as client:
            # Verify service exists and get metadata
            try:
                service_data = await client.get(
                    "/service/{id}",
                    path_params={"id": service_id},
                    resource_type="service",
                    resource_id=str(service_id),
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Service '{service_id}' not found. "
                    "Use 'service_list' to see available services."
                )

            # Single export call
            export_data, _ = await client.download_service(
                str(service_id),
                options=_AI_MAP_EXPORT_OPTIONS,
            )

            data = _unwrap_export(export_data)

            # Build sections (client stays open for doc calls in later tasks)
            service_section = _build_service_section(
                data, service_data, instance, warnings
            )

            roles = data.get("roles", [])
            intended_flow = _build_intended_flow(roles, warnings)

            # Registrations summary from export (richer than service_get)
            export_registrations = data.get("registrations") or []
            registrations_summary = [
                {
                    "id": str(reg.get("id", "")),
                    "name": reg.get("name", ""),
                    "short_name": reg.get("shortName"),
                    "business_key": reg.get("businessKey"),
                    "active": reg.get("active", True),
                    "role_names": reg.get("roleNames") or [],
                    "institution": (reg.get("registrationInstitution") or {}).get(
                        "name"
                    ),
                }
                for reg in export_registrations
            ]
            service_section["registrations"] = registrations_summary

            forms_section = _build_forms_section(data, include_role_forms, warnings)

            # Document traversal (opt-in, default True)
            documents_section = None
            if include_documents:
                registrations = service_data.get("registrations", [])
                documents_section = await _build_documents_section(
                    client, registrations, warnings
                )

            result: dict[str, Any] = {
                "service": service_section,
                "intended_flow": intended_flow,
                "forms": forms_section,
                "warnings": warnings,
                "sources": {
                    "export": f"POST /download_service/{service_id}",
                    "export_options": _AI_MAP_EXPORT_OPTIONS,
                },
            }

            if documents_section is not None:
                result["documents"] = documents_section
                result["sources"]["document_requirements"] = (
                    "GET /registration/{id}/document_requirement (per registration)"
                )

            if include_raw_sources:
                result["sources"]["raw_export"] = data

    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service")

    return result


def register_service_map_tools(mcp: Any) -> None:
    """Register service map tools with the MCP server."""
    from mcp_eregistrations_bpa.tools.annotations import READ

    mcp.tool(annotations=READ)(service_ai_map)
