"""
TOM ↔ Canonical Control Mapping Routes.

Three-layer architecture:
  TOM Measures (~88, audit-level) → Mapping Bridge → Canonical Controls (10,000+)

Endpoints:
  POST /v1/tom-mappings/sync         — Sync canonical controls for company profile
  GET  /v1/tom-mappings              — List all mappings for tenant/project
  GET  /v1/tom-mappings/by-tom/{code} — Mappings for a specific TOM control
  GET  /v1/tom-mappings/stats        — Coverage statistics
  POST /v1/tom-mappings/manual       — Manually add a mapping
  DELETE /v1/tom-mappings/{id}       — Remove a mapping
"""

from __future__ import annotations

import hashlib
import json
import logging
from typing import Any, Optional

from fastapi import APIRouter, HTTPException, Query, Header
from pydantic import BaseModel
from sqlalchemy import text

from database import SessionLocal

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tom-mappings", tags=["tom-control-mappings"])


# =============================================================================
# TOM CATEGORY → CANONICAL CATEGORY MAPPING
# =============================================================================

# Maps 13 TOM control categories to canonical_control_categories
# Each TOM category maps to 1-3 canonical categories for broad coverage
TOM_TO_CANONICAL_CATEGORIES: dict[str, list[str]] = {
    "ACCESS_CONTROL":       ["authentication", "identity", "physical"],
    "ADMISSION_CONTROL":    ["authentication", "identity", "system"],
    "ACCESS_AUTHORIZATION": ["authentication", "identity"],
    "TRANSFER_CONTROL":     ["network", "data_protection", "encryption"],
    "INPUT_CONTROL":        ["application", "data_protection"],
    "ORDER_CONTROL":        ["supply_chain", "compliance"],
    "AVAILABILITY":         ["continuity", "system"],
    "SEPARATION":           ["network", "data_protection"],
    "ENCRYPTION":           ["encryption"],
    "PSEUDONYMIZATION":     ["data_protection", "encryption"],
    "RESILIENCE":           ["continuity", "system"],
    "RECOVERY":             ["continuity"],
    "REVIEW":               ["compliance", "governance", "risk"],
}


# =============================================================================
# REQUEST / RESPONSE MODELS
# =============================================================================

class SyncRequest(BaseModel):
    """Trigger a sync of canonical controls to TOM measures."""
    industry: Optional[str] = None
    company_size: Optional[str] = None
    force: bool = False


class ManualMappingRequest(BaseModel):
    """Manually add a canonical control to a TOM measure."""
    tom_control_code: str
    tom_category: str
    canonical_control_id: str
    canonical_control_code: str
    canonical_category: Optional[str] = None
    relevance_score: float = 1.0


# =============================================================================
# HELPERS
# =============================================================================

def _get_tenant_id(x_tenant_id: Optional[str]) -> str:
    """Extract tenant ID from header."""
    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="X-Tenant-ID header required")
    return x_tenant_id


def _compute_profile_hash(industry: Optional[str], company_size: Optional[str]) -> str:
    """Compute a hash from profile parameters for change detection."""
    data = json.dumps({"industry": industry, "company_size": company_size}, sort_keys=True)
    return hashlib.sha256(data.encode()).hexdigest()[:16]


def _mapping_row_to_dict(r) -> dict[str, Any]:
    """Convert a mapping row to API response dict."""
    return {
        "id": str(r.id),
        "tenant_id": str(r.tenant_id),
        "project_id": str(r.project_id) if r.project_id else None,
        "tom_control_code": r.tom_control_code,
        "tom_category": r.tom_category,
        "canonical_control_id": str(r.canonical_control_id),
        "canonical_control_code": r.canonical_control_code,
        "canonical_category": r.canonical_category,
        "mapping_type": r.mapping_type,
        "relevance_score": float(r.relevance_score) if r.relevance_score else 1.0,
        "created_at": r.created_at.isoformat() if r.created_at else None,
    }


# =============================================================================
# SYNC ENDPOINT
# =============================================================================

@router.post("/sync")
async def sync_mappings(
    body: SyncRequest,
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
    project_id: Optional[str] = Query(None),
):
    """
    Sync canonical controls to TOM measures based on company profile.

    Algorithm:
    1. Compute profile hash → skip if unchanged (unless force=True)
    2. For each TOM category, find matching canonical controls by:
       - Category mapping (TOM category → canonical categories)
       - Industry filter (applicable_industries JSONB containment)
       - Company size filter (applicable_company_size JSONB containment)
       - Only approved + customer_visible controls
    3. Delete old auto-mappings, insert new ones
    4. Update sync state
    """
    tenant_id = _get_tenant_id(x_tenant_id)
    profile_hash = _compute_profile_hash(body.industry, body.company_size)

    with SessionLocal() as db:
        # Check if sync is needed (profile unchanged)
        if not body.force:
            existing = db.execute(
                text("""
                    SELECT profile_hash FROM tom_control_sync_state
                    WHERE tenant_id = :tid AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
                """),
                {"tid": tenant_id, "pid": project_id},
            ).fetchone()
            if existing and existing.profile_hash == profile_hash:
                return {
                    "status": "unchanged",
                    "message": "Profile unchanged since last sync",
                    "profile_hash": profile_hash,
                }

        # Delete old auto-mappings for this tenant+project
        db.execute(
            text("""
                DELETE FROM tom_control_mappings
                WHERE tenant_id = :tid
                  AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
                  AND mapping_type = 'auto'
            """),
            {"tid": tenant_id, "pid": project_id},
        )

        total_mappings = 0
        canonical_ids_matched = set()
        tom_codes_covered = set()

        # For each TOM category, find matching canonical controls
        for tom_category, canonical_categories in TOM_TO_CANONICAL_CATEGORIES.items():
            # Build JSONB containment query for categories
            cat_conditions = " OR ".join(
                f"category = :cat_{i}" for i in range(len(canonical_categories))
            )
            cat_params = {f"cat_{i}": c for i, c in enumerate(canonical_categories)}

            # Build industry filter
            industry_filter = ""
            if body.industry:
                industry_filter = """
                    AND (
                        applicable_industries IS NULL
                        OR applicable_industries @> '"all"'::jsonb
                        OR applicable_industries @> (:industry)::jsonb
                    )
                """
                cat_params["industry"] = json.dumps([body.industry])

            # Build company size filter
            size_filter = ""
            if body.company_size:
                size_filter = """
                    AND (
                        applicable_company_size IS NULL
                        OR applicable_company_size @> '"all"'::jsonb
                        OR applicable_company_size @> (:csize)::jsonb
                    )
                """
                cat_params["csize"] = json.dumps([body.company_size])

            query = f"""
                SELECT id, control_id, category
                FROM canonical_controls
                WHERE ({cat_conditions})
                  AND release_state = 'approved'
                  AND customer_visible = true
                  {industry_filter}
                  {size_filter}
                ORDER BY control_id
            """

            rows = db.execute(text(query), cat_params).fetchall()

            # Find TOM control codes in this category (query the frontend library
            # codes; we use the category prefix pattern from the loader)
            # TOM codes follow pattern: TOM-XX-NN where XX is category abbreviation
            # We insert one mapping per canonical control per TOM category
            for row in rows:
                db.execute(
                    text("""
                        INSERT INTO tom_control_mappings (
                            tenant_id, project_id, tom_control_code, tom_category,
                            canonical_control_id, canonical_control_code, canonical_category,
                            mapping_type, relevance_score
                        ) VALUES (
                            :tid, :pid, :tom_cat, :tom_cat,
                            :cc_id, :cc_code, :cc_category,
                            'auto', 1.00
                        )
                        ON CONFLICT (tenant_id, project_id, tom_control_code, canonical_control_id)
                        DO NOTHING
                    """),
                    {
                        "tid": tenant_id,
                        "pid": project_id,
                        "tom_cat": tom_category,
                        "cc_id": str(row.id),
                        "cc_code": row.control_id,
                        "cc_category": row.category,
                    },
                )
                total_mappings += 1
                canonical_ids_matched.add(str(row.id))
                tom_codes_covered.add(tom_category)

        # Upsert sync state
        db.execute(
            text("""
                INSERT INTO tom_control_sync_state (
                    tenant_id, project_id, profile_hash,
                    total_mappings, canonical_controls_matched, tom_controls_covered,
                    last_synced_at
                ) VALUES (
                    :tid, :pid, :hash,
                    :total, :matched, :covered,
                    NOW()
                )
                ON CONFLICT (tenant_id, project_id)
                DO UPDATE SET
                    profile_hash = :hash,
                    total_mappings = :total,
                    canonical_controls_matched = :matched,
                    tom_controls_covered = :covered,
                    last_synced_at = NOW()
            """),
            {
                "tid": tenant_id,
                "pid": project_id,
                "hash": profile_hash,
                "total": total_mappings,
                "matched": len(canonical_ids_matched),
                "covered": len(tom_codes_covered),
            },
        )

        db.commit()

    return {
        "status": "synced",
        "profile_hash": profile_hash,
        "total_mappings": total_mappings,
        "canonical_controls_matched": len(canonical_ids_matched),
        "tom_categories_covered": len(tom_codes_covered),
    }


# =============================================================================
# LIST MAPPINGS
# =============================================================================

@router.get("")
async def list_mappings(
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
    project_id: Optional[str] = Query(None),
    tom_category: Optional[str] = Query(None),
    mapping_type: Optional[str] = Query(None),
    limit: int = Query(500, ge=1, le=5000),
    offset: int = Query(0, ge=0),
):
    """List all TOM ↔ canonical control mappings for tenant/project."""
    tenant_id = _get_tenant_id(x_tenant_id)

    query = """
        SELECT m.*, cc.title as canonical_title, cc.severity as canonical_severity
        FROM tom_control_mappings m
        LEFT JOIN canonical_controls cc ON cc.id = m.canonical_control_id
        WHERE m.tenant_id = :tid
          AND (m.project_id = :pid OR (m.project_id IS NULL AND :pid IS NULL))
    """
    params: dict[str, Any] = {"tid": tenant_id, "pid": project_id}

    if tom_category:
        query += " AND m.tom_category = :tcat"
        params["tcat"] = tom_category
    if mapping_type:
        query += " AND m.mapping_type = :mtype"
        params["mtype"] = mapping_type

    query += " ORDER BY m.tom_category, m.canonical_control_code"
    query += " LIMIT :lim OFFSET :off"
    params["lim"] = limit
    params["off"] = offset

    count_query = """
        SELECT count(*) FROM tom_control_mappings
        WHERE tenant_id = :tid
          AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
    """
    count_params: dict[str, Any] = {"tid": tenant_id, "pid": project_id}
    if tom_category:
        count_query += " AND tom_category = :tcat"
        count_params["tcat"] = tom_category

    with SessionLocal() as db:
        rows = db.execute(text(query), params).fetchall()
        total = db.execute(text(count_query), count_params).scalar()

    mappings = []
    for r in rows:
        d = _mapping_row_to_dict(r)
        d["canonical_title"] = getattr(r, "canonical_title", None)
        d["canonical_severity"] = getattr(r, "canonical_severity", None)
        mappings.append(d)

    return {"mappings": mappings, "total": total}


# =============================================================================
# MAPPINGS BY TOM CONTROL
# =============================================================================

@router.get("/by-tom/{tom_code}")
async def get_mappings_by_tom(
    tom_code: str,
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
    project_id: Optional[str] = Query(None),
):
    """Get all canonical controls mapped to a specific TOM control code or category."""
    tenant_id = _get_tenant_id(x_tenant_id)

    with SessionLocal() as db:
        rows = db.execute(
            text("""
                SELECT m.*, cc.title as canonical_title, cc.severity as canonical_severity,
                       cc.objective as canonical_objective
                FROM tom_control_mappings m
                LEFT JOIN canonical_controls cc ON cc.id = m.canonical_control_id
                WHERE m.tenant_id = :tid
                  AND (m.project_id = :pid OR (m.project_id IS NULL AND :pid IS NULL))
                  AND (m.tom_control_code = :code OR m.tom_category = :code)
                ORDER BY m.canonical_control_code
            """),
            {"tid": tenant_id, "pid": project_id, "code": tom_code},
        ).fetchall()

    mappings = []
    for r in rows:
        d = _mapping_row_to_dict(r)
        d["canonical_title"] = getattr(r, "canonical_title", None)
        d["canonical_severity"] = getattr(r, "canonical_severity", None)
        d["canonical_objective"] = getattr(r, "canonical_objective", None)
        mappings.append(d)

    return {"tom_code": tom_code, "mappings": mappings, "total": len(mappings)}


# =============================================================================
# STATS
# =============================================================================

@router.get("/stats")
async def get_mapping_stats(
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
    project_id: Optional[str] = Query(None),
):
    """Coverage statistics for TOM ↔ canonical control mappings."""
    tenant_id = _get_tenant_id(x_tenant_id)

    with SessionLocal() as db:
        # Sync state
        sync_state = db.execute(
            text("""
                SELECT * FROM tom_control_sync_state
                WHERE tenant_id = :tid
                  AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
            """),
            {"tid": tenant_id, "pid": project_id},
        ).fetchone()

        # Per-category breakdown
        category_stats = db.execute(
            text("""
                SELECT tom_category,
                       count(*) as total_mappings,
                       count(DISTINCT canonical_control_id) as unique_controls,
                       count(*) FILTER (WHERE mapping_type = 'auto') as auto_count,
                       count(*) FILTER (WHERE mapping_type = 'manual') as manual_count
                FROM tom_control_mappings
                WHERE tenant_id = :tid
                  AND (project_id = :pid OR (project_id IS NULL AND :pid IS NULL))
                GROUP BY tom_category
                ORDER BY tom_category
            """),
            {"tid": tenant_id, "pid": project_id},
        ).fetchall()

        # Total canonical controls in DB (approved + visible)
        total_canonical = db.execute(
            text("""
                SELECT count(*) FROM canonical_controls
                WHERE release_state = 'approved' AND customer_visible = true
            """)
        ).scalar()

    return {
        "sync_state": {
            "profile_hash": sync_state.profile_hash if sync_state else None,
            "total_mappings": sync_state.total_mappings if sync_state else 0,
            "canonical_controls_matched": sync_state.canonical_controls_matched if sync_state else 0,
            "tom_controls_covered": sync_state.tom_controls_covered if sync_state else 0,
            "last_synced_at": sync_state.last_synced_at.isoformat() if sync_state and sync_state.last_synced_at else None,
        },
        "category_breakdown": [
            {
                "tom_category": r.tom_category,
                "total_mappings": r.total_mappings,
                "unique_controls": r.unique_controls,
                "auto_count": r.auto_count,
                "manual_count": r.manual_count,
            }
            for r in category_stats
        ],
        "total_canonical_controls_available": total_canonical or 0,
    }


# =============================================================================
# MANUAL MAPPING
# =============================================================================

@router.post("/manual", status_code=201)
async def add_manual_mapping(
    body: ManualMappingRequest,
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
    project_id: Optional[str] = Query(None),
):
    """Manually add a canonical control to a TOM measure."""
    tenant_id = _get_tenant_id(x_tenant_id)

    with SessionLocal() as db:
        # Verify canonical control exists
        cc = db.execute(
            text("SELECT id, control_id, category FROM canonical_controls WHERE id = CAST(:cid AS uuid)"),
            {"cid": body.canonical_control_id},
        ).fetchone()
        if not cc:
            raise HTTPException(status_code=404, detail="Canonical control not found")

        try:
            row = db.execute(
                text("""
                    INSERT INTO tom_control_mappings (
                        tenant_id, project_id, tom_control_code, tom_category,
                        canonical_control_id, canonical_control_code, canonical_category,
                        mapping_type, relevance_score
                    ) VALUES (
                        :tid, :pid, :tom_code, :tom_cat,
                        CAST(:cc_id AS uuid), :cc_code, :cc_category,
                        'manual', :score
                    )
                    RETURNING *
                """),
                {
                    "tid": tenant_id,
                    "pid": project_id,
                    "tom_code": body.tom_control_code,
                    "tom_cat": body.tom_category,
                    "cc_id": body.canonical_control_id,
                    "cc_code": body.canonical_control_code,
                    "cc_category": body.canonical_category or cc.category,
                    "score": body.relevance_score,
                },
            ).fetchone()
            db.commit()
        except Exception as e:
            if "unique" in str(e).lower() or "duplicate" in str(e).lower():
                raise HTTPException(status_code=409, detail="Mapping already exists")
            raise

    return _mapping_row_to_dict(row)


# =============================================================================
# DELETE MAPPING
# =============================================================================

@router.delete("/{mapping_id}", status_code=204)
async def delete_mapping(
    mapping_id: str,
    x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
):
    """Remove a mapping (manual or auto)."""
    tenant_id = _get_tenant_id(x_tenant_id)

    with SessionLocal() as db:
        result = db.execute(
            text("""
                DELETE FROM tom_control_mappings
                WHERE id = CAST(:mid AS uuid) AND tenant_id = :tid
            """),
            {"mid": mapping_id, "tid": tenant_id},
        )
        if result.rowcount == 0:
            raise HTTPException(status_code=404, detail="Mapping not found")
        db.commit()

    return None
