from __future__ import annotations

import hashlib
import json
import uuid
from dataclasses import dataclass
from typing import Any

from sqlalchemy.orm import Session

from govoplan_core.admin.settings import get_system_settings
from govoplan_core.core.change_sequence import record_change
from govoplan_core.core.configuration_safety import classify_configuration_field, plan_configuration_change
from govoplan_core.core.maintenance import saved_maintenance_mode
from govoplan_core.security.redaction import redact_secret_values
from govoplan_core.security.time import utc_now


CONFIGURATION_CONTROL_SETTINGS_KEY = "_configuration_control"
CONFIGURATION_CONTROL_MODULE_ID = "core"
CONFIGURATION_CHANGES_COLLECTION = "core.configuration_changes"
CONFIGURATION_CHANGE_REQUEST_RESOURCE = "configuration_change_request"
CONFIGURATION_CHANGE_RECORD_RESOURCE = "configuration_change_record"
MAX_CONFIGURATION_CHANGE_HISTORY = 200
MAX_CONFIGURATION_CHANGE_REQUESTS = 200


class ConfigurationControlError(Exception):
    def __init__(self, message: str, *, code: str = "configuration_change_blocked", plan: dict[str, Any] | None = None) -> None:
        super().__init__(message)
        self.code = code
        self.plan = plan or {}


@dataclass(frozen=True, slots=True)
class ConfigurationChangeApproval:
    request: dict[str, Any] | None
    plan: dict[str, Any]


def configuration_value_digest(value: object) -> str:
    payload = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()


def configuration_control_snapshot(session: Session) -> dict[str, list[dict[str, Any]]]:
    item = get_system_settings(session)
    control = _control_payload(item.settings or {})
    return {
        "requests": list(control.get("requests", [])),
        "history": list(control.get("history", [])),
    }


def create_configuration_change_request(
    session: Session,
    *,
    key: str,
    value: object,
    actor_user_id: str,
    actor_scopes: tuple[str, ...] | list[str],
    dry_run: bool,
    target: dict[str, Any] | None = None,
    reason: str | None = None,
) -> dict[str, Any]:
    field = classify_configuration_field(key)
    if field is None:
        raise ConfigurationControlError(
            "This setting is not in the configuration safety catalog.",
            code="unknown_configuration_field",
            plan=plan_configuration_change(key, actor_scopes=actor_scopes, value=value).to_dict(),
        )
    if not field.ui_managed:
        raise ConfigurationControlError(
            "This setting is deployment-managed and cannot be edited through the UI.",
            code="deployment_managed",
            plan=plan_configuration_change(key, actor_scopes=actor_scopes, value=value).to_dict(),
        )

    item = get_system_settings(session)
    settings = dict(item.settings or {})
    control = _control_payload(settings)
    now = _now()
    request = {
        "id": f"cfgreq-{uuid.uuid4().hex}",
        "key": field.key,
        "label": field.label,
        "target": dict(target or {}),
        "value_hash": configuration_value_digest(value),
        "value_preview": _sanitize_value(field.key, value),
        "dry_run": bool(dry_run),
        "requested_by": actor_user_id,
        "requested_at": now,
        "updated_at": now,
        "reason": reason.strip() if isinstance(reason, str) and reason.strip() else None,
        "status": "pending_approval" if field.two_person_approval_required else "approved",
        "approvals": [
            {
                "user_id": actor_user_id,
                "approved_at": now,
                "role": "requester",
            }
        ],
        "plan": plan_configuration_change(
            field.key,
            actor_scopes=actor_scopes,
            value=value,
            dry_run=bool(dry_run),
            maintenance_mode=saved_maintenance_mode(session).enabled,
            approval_count=1,
            include_env_only=False,
        ).to_dict(),
    }
    control["requests"] = _trim_requests([request, *list(control.get("requests", []))])
    settings[CONFIGURATION_CONTROL_SETTINGS_KEY] = control
    item.settings = settings
    session.add(item)
    session.flush()
    _record_configuration_control_change(
        session,
        resource_type=CONFIGURATION_CHANGE_REQUEST_RESOURCE,
        resource_id=str(request["id"]),
        operation="created",
        actor_user_id=actor_user_id,
        payload={"key": field.key, "status": request["status"]},
    )
    return dict(request)


def approve_configuration_change_request(
    session: Session,
    *,
    request_id: str,
    actor_user_id: str,
    actor_scopes: tuple[str, ...] | list[str],
    reason: str | None = None,
) -> dict[str, Any]:
    item = get_system_settings(session)
    settings = dict(item.settings or {})
    control = _control_payload(settings)
    requests = list(control.get("requests", []))
    index, request = _find_request(requests, request_id)
    if request is None:
        raise ConfigurationControlError("Configuration change request not found.", code="configuration_request_not_found")
    if request.get("status") in {"applied", "rejected"}:
        raise ConfigurationControlError("Configuration change request is no longer approvable.", code="configuration_request_closed")
    if request.get("requested_by") == actor_user_id:
        raise ConfigurationControlError("The requester cannot be the second approver.", code="second_approver_required")
    approvals = list(request.get("approvals", []))
    if not any(item.get("user_id") == actor_user_id for item in approvals if isinstance(item, dict)):
        approvals.append(
            {
                "user_id": actor_user_id,
                "approved_at": _now(),
                "role": "approver",
                "reason": reason.strip() if isinstance(reason, str) and reason.strip() else None,
            }
        )
    request = dict(request)
    request["approvals"] = approvals
    request["updated_at"] = _now()
    request["status"] = "approved" if _approval_count(request) >= 2 else "pending_approval"
    request["plan"] = plan_configuration_change(
        str(request.get("key") or ""),
        actor_scopes=actor_scopes,
        value=request.get("value_preview"),
        dry_run=bool(request.get("dry_run")),
        maintenance_mode=saved_maintenance_mode(session).enabled,
        approval_count=_approval_count(request),
        include_env_only=False,
    ).to_dict()
    requests[index] = request
    control["requests"] = _trim_requests(requests)
    settings[CONFIGURATION_CONTROL_SETTINGS_KEY] = control
    item.settings = settings
    session.add(item)
    session.flush()
    _record_configuration_control_change(
        session,
        resource_type=CONFIGURATION_CHANGE_REQUEST_RESOURCE,
        resource_id=str(request["id"]),
        operation="updated",
        actor_user_id=actor_user_id,
        payload={"key": request.get("key"), "status": request.get("status")},
    )
    return dict(request)


def ensure_configuration_change_allowed(
    session: Session,
    *,
    key: str,
    value: object,
    actor_user_id: str,
    actor_scopes: tuple[str, ...] | list[str],
    change_request_id: str | None = None,
    target: dict[str, Any] | None = None,
    dry_run: bool = False,
) -> ConfigurationChangeApproval:
    field = classify_configuration_field(key)
    if field is None:
        plan = plan_configuration_change(key, actor_scopes=actor_scopes, value=value).to_dict()
        raise ConfigurationControlError("This setting is not in the configuration safety catalog.", code="unknown_configuration_field", plan=plan)

    request: dict[str, Any] | None = None
    approval_count = 0
    dry_run_satisfied = dry_run
    requires_control = field.ui_managed and (field.dry_run_required or field.two_person_approval_required or field.maintenance_required)
    if change_request_id:
        request = _request_by_id(session, change_request_id)
        if request is None:
            raise ConfigurationControlError("Configuration change request not found.", code="configuration_request_not_found")
        if request.get("status") in {"applied", "rejected"}:
            raise ConfigurationControlError("Configuration change request is closed.", code="configuration_request_closed")
        if request.get("key") != field.key:
            raise ConfigurationControlError("Configuration change request does not match this setting.", code="configuration_request_mismatch")
        if request.get("value_hash") != configuration_value_digest(value):
            raise ConfigurationControlError("Configuration change request value does not match this apply payload.", code="configuration_value_mismatch")
        approval_count = _approval_count(request)
        dry_run_satisfied = bool(request.get("dry_run")) or dry_run_satisfied
    elif requires_control:
        plan = plan_configuration_change(
            field.key,
            actor_scopes=actor_scopes,
            value=value,
            dry_run=dry_run_satisfied,
            maintenance_mode=saved_maintenance_mode(session).enabled,
            approval_count=approval_count,
            include_env_only=False,
        ).to_dict()
        raise ConfigurationControlError(
            "This configuration change requires a recorded change request.",
            code="configuration_request_required",
            plan=plan,
        )

    plan = plan_configuration_change(
        field.key,
        actor_scopes=actor_scopes,
        value=value,
        dry_run=dry_run_satisfied,
        maintenance_mode=saved_maintenance_mode(session).enabled,
        approval_count=approval_count,
        include_env_only=False,
    ).to_dict()
    if plan.get("blockers"):
        raise ConfigurationControlError("Configuration change is blocked by safety guardrails.", plan=plan)
    return ConfigurationChangeApproval(request=request, plan=plan)


def record_configuration_change_applied(
    session: Session,
    *,
    key: str,
    before_value: object,
    after_value: object,
    actor_user_id: str,
    approval: ConfigurationChangeApproval | None = None,
    target: dict[str, Any] | None = None,
    audit_event: str | None = None,
) -> dict[str, Any]:
    item = get_system_settings(session)
    settings = dict(item.settings or {})
    control = _control_payload(settings)
    sequence = int(control.get("sequence") or 0) + 1
    request = approval.request if approval else None
    record = {
        "id": f"cfgchg-{sequence:06d}-{uuid.uuid4().hex[:8]}",
        "version": sequence,
        "key": key,
        "target": dict(target or {}),
        "actor_user_id": actor_user_id,
        "approval_request_id": request.get("id") if request else None,
        "approval_user_ids": [item.get("user_id") for item in request.get("approvals", []) if isinstance(item, dict)] if request else [],
        "audit_event": audit_event,
        "before": _sanitize_value(key, before_value),
        "after": _sanitize_value(key, after_value),
        "rollback_value": _sanitize_value(key, before_value),
        "plan": approval.plan if approval else plan_configuration_change(key, actor_scopes=(), value=after_value).to_dict(),
        "created_at": _now(),
        "status": "applied",
    }
    control["sequence"] = sequence
    control["history"] = _trim_history([record, *list(control.get("history", []))])
    if request:
        requests = list(control.get("requests", []))
        index, stored = _find_request(requests, str(request.get("id")))
        if stored is not None:
            stored = dict(stored)
            stored["status"] = "applied"
            stored["applied_change_id"] = record["id"]
            stored["updated_at"] = _now()
            requests[index] = stored
            control["requests"] = _trim_requests(requests)
    settings[CONFIGURATION_CONTROL_SETTINGS_KEY] = control
    item.settings = settings
    session.add(item)
    session.flush()
    _record_configuration_control_change(
        session,
        resource_type=CONFIGURATION_CHANGE_RECORD_RESOURCE,
        resource_id=str(record["id"]),
        operation="created",
        actor_user_id=actor_user_id,
        payload={"key": key, "status": record["status"], "version": record["version"]},
    )
    if request:
        _record_configuration_control_change(
            session,
            resource_type=CONFIGURATION_CHANGE_REQUEST_RESOURCE,
            resource_id=str(request.get("id")),
            operation="updated",
            actor_user_id=actor_user_id,
            payload={"key": key, "status": "applied", "applied_change_id": record["id"]},
        )
    return dict(record)


def _record_configuration_control_change(
    session: Session,
    *,
    resource_type: str,
    resource_id: str,
    operation: str,
    actor_user_id: str,
    payload: dict[str, Any],
) -> None:
    record_change(
        session,
        module_id=CONFIGURATION_CONTROL_MODULE_ID,
        collection=CONFIGURATION_CHANGES_COLLECTION,
        resource_type=resource_type,
        resource_id=resource_id,
        operation=operation,
        actor_type="user",
        actor_id=actor_user_id,
        payload=payload,
    )


def _request_by_id(session: Session, request_id: str) -> dict[str, Any] | None:
    item = get_system_settings(session)
    _index, request = _find_request(list(_control_payload(item.settings or {}).get("requests", [])), request_id)
    return dict(request) if request is not None else None


def _find_request(requests: list[dict[str, Any]], request_id: str) -> tuple[int, dict[str, Any] | None]:
    for index, request in enumerate(requests):
        if request.get("id") == request_id:
            return index, request
    return -1, None


def _control_payload(settings: dict[str, Any]) -> dict[str, Any]:
    raw = settings.get(CONFIGURATION_CONTROL_SETTINGS_KEY)
    if not isinstance(raw, dict):
        return {"sequence": 0, "requests": [], "history": []}
    return {
        "sequence": int(raw.get("sequence") or 0),
        "requests": [dict(item) for item in raw.get("requests", []) if isinstance(item, dict)],
        "history": [dict(item) for item in raw.get("history", []) if isinstance(item, dict)],
    }


def _approval_count(request: dict[str, Any]) -> int:
    user_ids = []
    for item in request.get("approvals", []):
        if isinstance(item, dict) and item.get("user_id"):
            user_ids.append(str(item["user_id"]))
    return len(set(user_ids))


def _sanitize_value(key: str, value: object) -> object:
    field = classify_configuration_field(key)
    if field is not None and field.secret_handling in {"reference_only", "env_only"}:
        return _redact_secrets(value)
    return _redact_secrets(value)


def _redact_secrets(value: object) -> object:
    return redact_secret_values(value)


def _trim_requests(requests: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return requests[:MAX_CONFIGURATION_CHANGE_REQUESTS]


def _trim_history(history: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return history[:MAX_CONFIGURATION_CHANGE_HISTORY]


def _now() -> str:
    return utc_now().isoformat()


__all__ = [
    "ConfigurationChangeApproval",
    "ConfigurationControlError",
    "CONFIGURATION_CHANGE_RECORD_RESOURCE",
    "CONFIGURATION_CHANGE_REQUEST_RESOURCE",
    "CONFIGURATION_CHANGES_COLLECTION",
    "CONFIGURATION_CONTROL_MODULE_ID",
    "approve_configuration_change_request",
    "configuration_control_snapshot",
    "configuration_value_digest",
    "create_configuration_change_request",
    "ensure_configuration_change_allowed",
    "record_configuration_change_applied",
]
