# mypy: disable-error-code="arg-type,assignment,no-any-return,union-attr"
"""
Canonical Control Library service — framework + control CRUD with raw SQL.

Phase 1 Step 4: extracted from ``compliance.api.canonical_control_routes``.
Uses raw SQL via ``sqlalchemy.text()`` because the underlying tables
(``canonical_control_frameworks``, ``canonical_controls``) have no
SQLAlchemy model in this repo.
"""

import json
import re
from typing import Any, Optional

from sqlalchemy import text
from sqlalchemy.orm import Session

from compliance.domain import (
    ConflictError,
    NotFoundError,
    ValidationError,
)
from compliance.schemas.canonical_control import (
    ControlCreateRequest,
    ControlUpdateRequest,
    SimilarityCheckRequest,
)

_VALID_SEVERITIES = ("low", "medium", "high", "critical")
_CONTROL_ID_RE = re.compile(r"^[A-Z]{2,6}-[0-9]{3}$")
_JSON_CONTROL_FIELDS = {
    "scope", "requirements", "test_procedure", "evidence", "open_anchors", "tags",
}

_CONTROL_COLUMNS = """
    id, framework_id, control_id, title, objective, rationale,
    scope, requirements, test_procedure, evidence,
    severity, risk_score, implementation_effort, evidence_confidence,
    open_anchors, release_state, tags, created_at, updated_at
"""


def _ensure_list(val: Any) -> list:
    """Ensure a JSONB value is always a Python list."""
    if isinstance(val, list):
        return val
    if val is None:
        return []
    if isinstance(val, str):
        try:
            import json
            parsed = json.loads(val)
            return parsed if isinstance(parsed, list) else []
        except (json.JSONDecodeError, TypeError):
            return []
    return []


def _control_row(r: Any) -> dict[str, Any]:
    """Serialize a canonical_controls SELECT row to a response dict."""
    return {
        "id": str(r.id),
        "framework_id": str(r.framework_id),
        "control_id": r.control_id,
        "title": r.title,
        "objective": r.objective,
        "rationale": r.rationale,
        "scope": r.scope if isinstance(r.scope, dict) else {},
        "requirements": _ensure_list(r.requirements),
        "test_procedure": _ensure_list(r.test_procedure),
        "evidence": _ensure_list(r.evidence),
        "severity": r.severity,
        "risk_score": float(r.risk_score) if r.risk_score is not None else None,
        "implementation_effort": r.implementation_effort,
        "evidence_confidence": (
            float(r.evidence_confidence) if r.evidence_confidence is not None else None
        ),
        "open_anchors": _ensure_list(r.open_anchors),
        "release_state": r.release_state,
        "tags": _ensure_list(r.tags),
        "created_at": r.created_at.isoformat() if r.created_at else None,
        "updated_at": r.updated_at.isoformat() if r.updated_at else None,
    }


def _framework_row(r: Any) -> dict[str, Any]:
    return {
        "id": str(r.id),
        "framework_id": r.framework_id,
        "name": r.name,
        "version": r.version,
        "description": r.description,
        "owner": r.owner,
        "policy_version": r.policy_version,
        "release_state": r.release_state,
        "created_at": r.created_at.isoformat() if r.created_at else None,
        "updated_at": r.updated_at.isoformat() if r.updated_at else None,
    }


def _validate_control_input(
    severity: Optional[str], risk_score: Optional[float], control_id: Optional[str] = None
) -> None:
    if control_id is not None and not _CONTROL_ID_RE.match(control_id):
        raise ValidationError("control_id must match DOMAIN-NNN (e.g. AUTH-001)")
    if severity is not None and severity not in _VALID_SEVERITIES:
        raise ValidationError("severity must be low/medium/high/critical")
    if risk_score is not None and not (0 <= risk_score <= 10):
        raise ValidationError("risk_score must be 0..10")


class CanonicalControlService:
    """Business logic for the canonical control library."""

    def __init__(self, db: Session) -> None:
        self.db = db

    # ------------------------------------------------------------------
    # Frameworks
    # ------------------------------------------------------------------

    def list_frameworks(self) -> list[dict[str, Any]]:
        rows = self.db.execute(
            text("""
                SELECT id, framework_id, name, version, description,
                       owner, policy_version, release_state,
                       created_at, updated_at
                FROM canonical_control_frameworks
                ORDER BY name
            """)
        ).fetchall()
        return [_framework_row(r) for r in rows]

    def get_framework(self, framework_id: str) -> dict[str, Any]:
        row = self.db.execute(
            text("""
                SELECT id, framework_id, name, version, description,
                       owner, policy_version, release_state,
                       created_at, updated_at
                FROM canonical_control_frameworks
                WHERE framework_id = :fid
            """),
            {"fid": framework_id},
        ).fetchone()
        if not row:
            raise NotFoundError("Framework not found")
        return _framework_row(row)

    def list_framework_controls(
        self, framework_id: str, severity: Optional[str], release_state: Optional[str]
    ) -> list[dict[str, Any]]:
        fw = self.db.execute(
            text("SELECT id FROM canonical_control_frameworks WHERE framework_id = :fid"),
            {"fid": framework_id},
        ).fetchone()
        if not fw:
            raise NotFoundError("Framework not found")

        query = f"SELECT {_CONTROL_COLUMNS} FROM canonical_controls WHERE framework_id = :fw_id"
        params: dict[str, Any] = {"fw_id": str(fw.id)}
        if severity:
            query += " AND severity = :sev"
            params["sev"] = severity
        if release_state:
            query += " AND release_state = :rs"
            params["rs"] = release_state
        query += " ORDER BY control_id"
        rows = self.db.execute(text(query), params).fetchall()
        return [_control_row(r) for r in rows]

    # ------------------------------------------------------------------
    # Controls
    # ------------------------------------------------------------------

    def list_controls(
        self,
        severity: Optional[str],
        domain: Optional[str],
        release_state: Optional[str],
    ) -> list[dict[str, Any]]:
        query = f"SELECT {_CONTROL_COLUMNS} FROM canonical_controls WHERE 1=1"
        params: dict[str, Any] = {}
        if severity:
            query += " AND severity = :sev"
            params["sev"] = severity
        if domain:
            query += " AND LEFT(control_id, LENGTH(:dom)) = :dom"
            params["dom"] = domain.upper()
        if release_state:
            query += " AND release_state = :rs"
            params["rs"] = release_state
        query += " ORDER BY control_id"
        rows = self.db.execute(text(query), params).fetchall()
        return [_control_row(r) for r in rows]

    def get_control(self, control_id: str) -> dict[str, Any]:
        row = self.db.execute(
            text(f"SELECT {_CONTROL_COLUMNS} FROM canonical_controls WHERE control_id = :cid"),
            {"cid": control_id.upper()},
        ).fetchone()
        if not row:
            raise NotFoundError("Control not found")
        return _control_row(row)

    def create_control(self, body: ControlCreateRequest) -> dict[str, Any]:
        _validate_control_input(body.severity, body.risk_score, body.control_id)

        fw = self.db.execute(
            text("SELECT id FROM canonical_control_frameworks WHERE framework_id = :fid"),
            {"fid": body.framework_id},
        ).fetchone()
        if not fw:
            raise NotFoundError(f"Framework '{body.framework_id}' not found")

        existing = self.db.execute(
            text(
                "SELECT id FROM canonical_controls "
                "WHERE framework_id = :fid AND control_id = :cid"
            ),
            {"fid": str(fw.id), "cid": body.control_id},
        ).fetchone()
        if existing:
            raise ConflictError(f"Control '{body.control_id}' already exists")

        row = self.db.execute(
            text(f"""
                INSERT INTO canonical_controls (
                    framework_id, control_id, title, objective, rationale,
                    scope, requirements, test_procedure, evidence,
                    severity, risk_score, implementation_effort, evidence_confidence,
                    open_anchors, release_state, tags
                ) VALUES (
                    :fw_id, :cid, :title, :objective, :rationale,
                    :scope::jsonb, :requirements::jsonb, :test_procedure::jsonb, :evidence::jsonb,
                    :severity, :risk_score, :effort, :confidence,
                    :anchors::jsonb, :release_state, :tags::jsonb
                )
                RETURNING {_CONTROL_COLUMNS}
            """),
            {
                "fw_id": str(fw.id),
                "cid": body.control_id,
                "title": body.title,
                "objective": body.objective,
                "rationale": body.rationale,
                "scope": json.dumps(body.scope),
                "requirements": json.dumps(body.requirements),
                "test_procedure": json.dumps(body.test_procedure),
                "evidence": json.dumps(body.evidence),
                "severity": body.severity,
                "risk_score": body.risk_score,
                "effort": body.implementation_effort,
                "confidence": body.evidence_confidence,
                "anchors": json.dumps(body.open_anchors),
                "release_state": body.release_state,
                "tags": json.dumps(body.tags),
            },
        ).fetchone()
        self.db.commit()
        return _control_row(row)

    def update_control(
        self, control_id: str, body: ControlUpdateRequest
    ) -> dict[str, Any]:
        updates = body.dict(exclude_none=True)
        if not updates:
            raise ValidationError("No fields to update")

        _validate_control_input(updates.get("severity"), updates.get("risk_score"))

        set_parts: list[str] = []
        params: dict[str, Any] = {"cid": control_id.upper()}
        for key, val in updates.items():
            if key in _JSON_CONTROL_FIELDS:
                set_parts.append(f"{key} = :{key}::jsonb")
                params[key] = json.dumps(val)
            else:
                set_parts.append(f"{key} = :{key}")
                params[key] = val
        set_parts.append("updated_at = NOW()")

        row = self.db.execute(
            text(f"""
                UPDATE canonical_controls
                SET {', '.join(set_parts)}
                WHERE control_id = :cid
                RETURNING {_CONTROL_COLUMNS}
            """),
            params,
        ).fetchone()
        if not row:
            raise NotFoundError("Control not found")
        self.db.commit()
        return _control_row(row)

    def delete_control(self, control_id: str) -> None:
        result: Any = self.db.execute(
            text("DELETE FROM canonical_controls WHERE control_id = :cid"),
            {"cid": control_id.upper()},
        )
        if result.rowcount == 0:
            raise NotFoundError("Control not found")
        self.db.commit()

    # ------------------------------------------------------------------
    # Similarity + sources + licenses
    # ------------------------------------------------------------------

    async def similarity_check(
        self, control_id: str, body: SimilarityCheckRequest
    ) -> dict[str, Any]:
        from compliance.services.similarity_detector import check_similarity

        report = await check_similarity(body.source_text, body.candidate_text)
        return {
            "control_id": control_id.upper(),
            "max_exact_run": report.max_exact_run,
            "token_overlap": report.token_overlap,
            "ngram_jaccard": report.ngram_jaccard,
            "embedding_cosine": report.embedding_cosine,
            "lcs_ratio": report.lcs_ratio,
            "status": report.status,
            "details": report.details,
        }

    def list_sources(self) -> Any:
        from compliance.services.license_gate import get_source_permissions
        return get_source_permissions(self.db)

    def list_licenses(self) -> Any:
        from compliance.services.license_gate import get_license_matrix
        return get_license_matrix(self.db)
