# mypy: disable-error-code="arg-type,assignment,union-attr,no-any-return,attr-defined,index,call-overload,type-arg,var-annotated,misc,call-arg,return-value,no-untyped-call,dict-item"
"""
Vendor compliance service — Vendors CRUD + stats + status patch.

Phase 1 Step 4: extracted from ``compliance.api.vendor_compliance_routes``.
Helpers (_now_iso, _ok, _parse_json, _ts, _get, _to_snake, _to_camel,
_vendor_to_response, camelCase maps) are shared by both vendor service
modules and re-exported from the routes module for legacy test imports.
"""

import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Optional

from sqlalchemy import text
from sqlalchemy.orm import Session

from compliance.domain import NotFoundError, ValidationError

logger = logging.getLogger(__name__)

# Default tenant UUID
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"

# ============================================================================
# Helpers (shared across vendor service modules)
# ============================================================================


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat() + "Z"


def _ok(data: Any, status_code: int = 200) -> dict:
    """Wrap response in {success, data, timestamp} envelope."""
    return {"success": True, "data": data, "timestamp": _now_iso()}


def _parse_json(val: Any, default: Any = None) -> Any:
    """Parse a JSONB/TEXT field -> Python object."""
    if val is None:
        return default if default is not None else None
    if isinstance(val, (dict, list)):
        return val
    if isinstance(val, str):
        try:
            return json.loads(val)
        except Exception:
            return default if default is not None else val
    return val


def _ts(val: Any) -> Optional[str]:
    """Timestamp -> ISO string or None."""
    if not val:
        return None
    if isinstance(val, str):
        return val
    return val.isoformat()


def _get(row: Any, key: str, default: Any = None) -> Any:
    """Safe row access."""
    try:
        v = row[key]
        return default if v is None and default is not None else v
    except (KeyError, IndexError):
        return default


# camelCase <-> snake_case conversion maps
_VENDOR_CAMEL_TO_SNAKE = {
    "legalForm": "legal_form",
    "serviceDescription": "service_description",
    "serviceCategory": "service_category",
    "dataAccessLevel": "data_access_level",
    "processingLocations": "processing_locations",
    "transferMechanisms": "transfer_mechanisms",
    "primaryContact": "primary_contact",
    "dpoContact": "dpo_contact",
    "securityContact": "security_contact",
    "contractTypes": "contract_types",
    "inherentRiskScore": "inherent_risk_score",
    "residualRiskScore": "residual_risk_score",
    "manualRiskAdjustment": "manual_risk_adjustment",
    "riskJustification": "risk_justification",
    "reviewFrequency": "review_frequency",
    "lastReviewDate": "last_review_date",
    "nextReviewDate": "next_review_date",
    "processingActivityIds": "processing_activity_ids",
    "contactName": "contact_name",
    "contactEmail": "contact_email",
    "contactPhone": "contact_phone",
    "contactDepartment": "contact_department",
    "tenantId": "tenant_id",
    "createdAt": "created_at",
    "updatedAt": "updated_at",
    "createdBy": "created_by",
    "vendorId": "vendor_id",
    "contractId": "contract_id",
    "controlId": "control_id",
    "controlDomain": "control_domain",
    "evidenceIds": "evidence_ids",
    "lastAssessedAt": "last_assessed_at",
    "lastAssessedBy": "last_assessed_by",
    "nextAssessmentDate": "next_assessment_date",
    "fileName": "file_name",
    "originalName": "original_name",
    "mimeType": "mime_type",
    "fileSize": "file_size",
    "storagePath": "storage_path",
    "documentType": "document_type",
    "previousVersionId": "previous_version_id",
    "effectiveDate": "effective_date",
    "expirationDate": "expiration_date",
    "autoRenewal": "auto_renewal",
    "renewalNoticePeriod": "renewal_notice_period",
    "terminationNoticePeriod": "termination_notice_period",
    "reviewStatus": "review_status",
    "reviewCompletedAt": "review_completed_at",
    "complianceScore": "compliance_score",
    "extractedText": "extracted_text",
    "pageCount": "page_count",
    "findingType": "finding_type",
    "dueDate": "due_date",
    "resolvedAt": "resolved_at",
    "resolvedBy": "resolved_by",
}

_VENDOR_SNAKE_TO_CAMEL = {v: k for k, v in _VENDOR_CAMEL_TO_SNAKE.items()}


def _to_snake(data: dict) -> dict:
    """Convert camelCase keys in data to snake_case for DB storage."""
    result = {}
    for k, v in data.items():
        snake = _VENDOR_CAMEL_TO_SNAKE.get(k, k)
        result[snake] = v
    return result


def _to_camel(data: dict) -> dict:
    """Convert snake_case keys to camelCase for frontend."""
    result = {}
    for k, v in data.items():
        camel = _VENDOR_SNAKE_TO_CAMEL.get(k, k)
        result[camel] = v
    return result


# ============================================================================
# Row -> Response converters
# ============================================================================


def _vendor_to_response(row: Any) -> dict:
    return _to_camel({
        "id": str(row["id"]),
        "tenant_id": row["tenant_id"],
        "name": row["name"],
        "legal_form": _get(row, "legal_form", ""),
        "country": _get(row, "country", ""),
        "address": _get(row, "address", ""),
        "website": _get(row, "website", ""),
        "role": _get(row, "role", "PROCESSOR"),
        "service_description": _get(row, "service_description", ""),
        "service_category": _get(row, "service_category", "OTHER"),
        "data_access_level": _get(row, "data_access_level", "NONE"),
        "processing_locations": _parse_json(_get(row, "processing_locations"), []),
        "transfer_mechanisms": _parse_json(_get(row, "transfer_mechanisms"), []),
        "certifications": _parse_json(_get(row, "certifications"), []),
        "primary_contact": _parse_json(_get(row, "primary_contact"), {}),
        "dpo_contact": _parse_json(_get(row, "dpo_contact"), {}),
        "security_contact": _parse_json(_get(row, "security_contact"), {}),
        "contract_types": _parse_json(_get(row, "contract_types"), []),
        "inherent_risk_score": _get(row, "inherent_risk_score", 50),
        "residual_risk_score": _get(row, "residual_risk_score", 50),
        "manual_risk_adjustment": _get(row, "manual_risk_adjustment"),
        "risk_justification": _get(row, "risk_justification", ""),
        "review_frequency": _get(row, "review_frequency", "ANNUAL"),
        "last_review_date": _ts(_get(row, "last_review_date")),
        "next_review_date": _ts(_get(row, "next_review_date")),
        "status": _get(row, "status", "ACTIVE"),
        "processing_activity_ids": _parse_json(
            _get(row, "processing_activity_ids"), [],
        ),
        "notes": _get(row, "notes", ""),
        "contact_name": _get(row, "contact_name", ""),
        "contact_email": _get(row, "contact_email", ""),
        "contact_phone": _get(row, "contact_phone", ""),
        "contact_department": _get(row, "contact_department", ""),
        "created_at": _ts(row["created_at"]),
        "updated_at": _ts(row["updated_at"]),
        "created_by": _get(row, "created_by", "system"),
    })


# ============================================================================
# VendorService
# ============================================================================


class VendorService:
    """Vendor CRUD + stats + status patch."""

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

    def get_stats(self, tenant_id: Optional[str] = None) -> dict:
        tid = tenant_id or DEFAULT_TENANT_ID
        result = self._db.execute(text("""
            SELECT
                COUNT(*) AS total,
                COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active,
                COUNT(*) FILTER (WHERE status = 'INACTIVE') AS inactive,
                COUNT(*) FILTER (WHERE status = 'PENDING_REVIEW') AS pending_review,
                COUNT(*) FILTER (WHERE status = 'TERMINATED') AS terminated,
                COALESCE(AVG(inherent_risk_score), 0) AS avg_inherent_risk,
                COALESCE(AVG(residual_risk_score), 0) AS avg_residual_risk,
                COUNT(*) FILTER (WHERE inherent_risk_score >= 75) AS high_risk_count
            FROM vendor_vendors
            WHERE tenant_id = :tid
        """), {"tid": tid})
        row = result.fetchone()
        if row is None:
            stats = {
                "total": 0, "active": 0, "inactive": 0,
                "pending_review": 0, "terminated": 0,
                "avg_inherent_risk": 0, "avg_residual_risk": 0,
                "high_risk_count": 0,
            }
        else:
            stats = {
                "total": row["total"] or 0,
                "active": row["active"] or 0,
                "inactive": row["inactive"] or 0,
                "pendingReview": row["pending_review"] or 0,
                "terminated": row["terminated"] or 0,
                "avgInherentRisk": round(
                    float(row["avg_inherent_risk"] or 0), 1,
                ),
                "avgResidualRisk": round(
                    float(row["avg_residual_risk"] or 0), 1,
                ),
                "highRiskCount": row["high_risk_count"] or 0,
            }
        return _ok(stats)

    def list_vendors(
        self,
        tenant_id: Optional[str] = None,
        status: Optional[str] = None,
        risk_level: Optional[str] = None,
        search: Optional[str] = None,
        skip: int = 0,
        limit: int = 100,
    ) -> dict:
        tid = tenant_id or DEFAULT_TENANT_ID
        where = ["tenant_id = :tid"]
        params: dict = {"tid": tid}

        if status:
            where.append("status = :status")
            params["status"] = status
        if risk_level:
            if risk_level == "HIGH":
                where.append("inherent_risk_score >= 75")
            elif risk_level == "MEDIUM":
                where.append(
                    "inherent_risk_score >= 40 AND inherent_risk_score < 75",
                )
            elif risk_level == "LOW":
                where.append("inherent_risk_score < 40")
        if search:
            where.append(
                "(name ILIKE :search OR service_description ILIKE :search)",
            )
            params["search"] = f"%{search}%"

        where_clause = " AND ".join(where)
        params["lim"] = limit
        params["off"] = skip

        rows = self._db.execute(text(f"""
            SELECT * FROM vendor_vendors
            WHERE {where_clause}
            ORDER BY created_at DESC
            LIMIT :lim OFFSET :off
        """), params).fetchall()

        count_row = self._db.execute(text(f"""
            SELECT COUNT(*) AS cnt FROM vendor_vendors WHERE {where_clause}
        """), {k: v for k, v in params.items() if k not in ("lim", "off")}).fetchone()
        total = count_row["cnt"] if count_row else 0

        return _ok({
            "items": [_vendor_to_response(r) for r in rows],
            "total": total,
        })

    def get_vendor(self, vendor_id: str) -> dict:
        row = self._db.execute(
            text("SELECT * FROM vendor_vendors WHERE id = :id"),
            {"id": vendor_id},
        ).fetchone()
        if not row:
            raise NotFoundError("Vendor not found")
        return _ok(_vendor_to_response(row))

    def create_vendor(self, body: dict) -> dict:
        data = _to_snake(body)
        vid = str(uuid.uuid4())
        tid = data.get("tenant_id", DEFAULT_TENANT_ID)
        now = datetime.now(timezone.utc).isoformat()

        self._db.execute(text("""
            INSERT INTO vendor_vendors (
                id, tenant_id, name, legal_form, country, address, website,
                role, service_description, service_category, data_access_level,
                processing_locations, transfer_mechanisms, certifications,
                primary_contact, dpo_contact, security_contact,
                contract_types, inherent_risk_score, residual_risk_score,
                manual_risk_adjustment, risk_justification,
                review_frequency, last_review_date, next_review_date,
                status, processing_activity_ids, notes,
                contact_name, contact_email, contact_phone,
                contact_department,
                created_at, updated_at, created_by
            ) VALUES (
                :id, :tenant_id, :name, :legal_form, :country, :address,
                :website, :role, :service_description, :service_category,
                :data_access_level,
                CAST(:processing_locations AS jsonb),
                CAST(:transfer_mechanisms AS jsonb),
                CAST(:certifications AS jsonb),
                CAST(:primary_contact AS jsonb),
                CAST(:dpo_contact AS jsonb),
                CAST(:security_contact AS jsonb),
                CAST(:contract_types AS jsonb),
                :inherent_risk_score, :residual_risk_score,
                :manual_risk_adjustment, :risk_justification,
                :review_frequency, :last_review_date, :next_review_date,
                :status, CAST(:processing_activity_ids AS jsonb), :notes,
                :contact_name, :contact_email, :contact_phone,
                :contact_department,
                :created_at, :updated_at, :created_by
            )
        """), {
            "id": vid, "tenant_id": tid,
            "name": data.get("name", ""),
            "legal_form": data.get("legal_form", ""),
            "country": data.get("country", ""),
            "address": data.get("address", ""),
            "website": data.get("website", ""),
            "role": data.get("role", "PROCESSOR"),
            "service_description": data.get("service_description", ""),
            "service_category": data.get("service_category", "OTHER"),
            "data_access_level": data.get("data_access_level", "NONE"),
            "processing_locations": json.dumps(
                data.get("processing_locations", []),
            ),
            "transfer_mechanisms": json.dumps(
                data.get("transfer_mechanisms", []),
            ),
            "certifications": json.dumps(data.get("certifications", [])),
            "primary_contact": json.dumps(data.get("primary_contact", {})),
            "dpo_contact": json.dumps(data.get("dpo_contact", {})),
            "security_contact": json.dumps(data.get("security_contact", {})),
            "contract_types": json.dumps(data.get("contract_types", [])),
            "inherent_risk_score": data.get("inherent_risk_score", 50),
            "residual_risk_score": data.get("residual_risk_score", 50),
            "manual_risk_adjustment": data.get("manual_risk_adjustment"),
            "risk_justification": data.get("risk_justification", ""),
            "review_frequency": data.get("review_frequency", "ANNUAL"),
            "last_review_date": data.get("last_review_date"),
            "next_review_date": data.get("next_review_date"),
            "status": data.get("status", "ACTIVE"),
            "processing_activity_ids": json.dumps(
                data.get("processing_activity_ids", []),
            ),
            "notes": data.get("notes", ""),
            "contact_name": data.get("contact_name", ""),
            "contact_email": data.get("contact_email", ""),
            "contact_phone": data.get("contact_phone", ""),
            "contact_department": data.get("contact_department", ""),
            "created_at": now, "updated_at": now,
            "created_by": data.get("created_by", "system"),
        })
        self._db.commit()

        row = self._db.execute(
            text("SELECT * FROM vendor_vendors WHERE id = :id"),
            {"id": vid},
        ).fetchone()
        return _ok(_vendor_to_response(row))

    def update_vendor(self, vendor_id: str, body: dict) -> dict:
        existing = self._db.execute(
            text("SELECT id FROM vendor_vendors WHERE id = :id"),
            {"id": vendor_id},
        ).fetchone()
        if not existing:
            raise NotFoundError("Vendor not found")

        data = _to_snake(body)
        now = datetime.now(timezone.utc).isoformat()

        allowed = [
            "name", "legal_form", "country", "address", "website",
            "role", "service_description", "service_category",
            "data_access_level",
            "inherent_risk_score", "residual_risk_score",
            "manual_risk_adjustment", "risk_justification",
            "review_frequency", "last_review_date", "next_review_date",
            "status", "notes",
            "contact_name", "contact_email", "contact_phone",
            "contact_department",
        ]
        jsonb_fields = [
            "processing_locations", "transfer_mechanisms", "certifications",
            "primary_contact", "dpo_contact", "security_contact",
            "contract_types", "processing_activity_ids",
        ]

        sets = ["updated_at = :updated_at"]
        params: dict = {"id": vendor_id, "updated_at": now}

        for col in allowed:
            if col in data:
                sets.append(f"{col} = :{col}")
                params[col] = data[col]

        for col in jsonb_fields:
            if col in data:
                sets.append(f"{col} = CAST(:{col} AS jsonb)")
                params[col] = json.dumps(data[col])

        self._db.execute(
            text(f"UPDATE vendor_vendors SET {', '.join(sets)} WHERE id = :id"),
            params,
        )
        self._db.commit()

        row = self._db.execute(
            text("SELECT * FROM vendor_vendors WHERE id = :id"),
            {"id": vendor_id},
        ).fetchone()
        return _ok(_vendor_to_response(row))

    def delete_vendor(self, vendor_id: str) -> dict:
        result = self._db.execute(
            text("DELETE FROM vendor_vendors WHERE id = :id"),
            {"id": vendor_id},
        )
        self._db.commit()
        if result.rowcount == 0:
            raise NotFoundError("Vendor not found")
        return _ok({"deleted": True})

    def patch_status(self, vendor_id: str, body: dict) -> dict:
        new_status = body.get("status")
        if not new_status:
            raise ValidationError("status is required")
        valid = {"ACTIVE", "INACTIVE", "PENDING_REVIEW", "TERMINATED"}
        if new_status not in valid:
            raise ValidationError(
                f"Invalid status. Must be one of: {', '.join(sorted(valid))}",
            )

        result = self._db.execute(text("""
            UPDATE vendor_vendors
            SET status = :status, updated_at = :now
            WHERE id = :id
        """), {
            "id": vendor_id,
            "status": new_status,
            "now": datetime.now(timezone.utc).isoformat(),
        })
        self._db.commit()
        if result.rowcount == 0:
            raise NotFoundError("Vendor not found")

        row = self._db.execute(
            text("SELECT * FROM vendor_vendors WHERE id = :id"),
            {"id": vendor_id},
        ).fetchone()
        return _ok(_vendor_to_response(row))
