from __future__ import annotations

import json
import re
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Mapping

from sqlalchemy import Boolean, DateTime, Index, JSON, String, Text, select
from sqlalchemy.orm import Mapped, Session, mapped_column

from govoplan_core.audit.logging import audit_event
from govoplan_core.core.change_sequence import record_change
from govoplan_core.db.base import Base, TimestampMixin, utcnow
from govoplan_core.security.redaction import is_sensitive_key, redact_secret_values
from govoplan_core.security.secrets import decrypt_secret, encrypt_secret


CREDENTIAL_SCOPE_TYPES = frozenset({"system", "tenant", "group", "user", "campaign"})
CREDENTIAL_KINDS = frozenset(
    {
        "username_password",
        "token",
        "oauth2",
        "api_key",
        "client_secret",
        "aws",
        "custom",
        "external_secret",
    }
)
_SECRET_PUBLIC_DATA_KEYS = frozenset(
    {
        "access_token",
        "api_key",
        "authorization",
        "bearer_token",
        "client_secret",
        "password",
        "passphrase",
        "private_key",
        "refresh_token",
        "secret",
        "secret_access_key",
        "session_token",
        "token",
    }
)


class CredentialEnvelopeError(RuntimeError):
    pass


class CredentialEnvelope(Base, TimestampMixin):
    __tablename__ = "core_credential_envelopes"
    __table_args__ = (
        Index("ix_core_credential_envelopes_scope", "tenant_id", "scope_type", "scope_id"),
        Index("ix_core_credential_envelopes_active", "tenant_id", "is_active", "deleted_at"),
    )

    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    tenant_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
    scope_type: Mapped[str] = mapped_column(String(20), nullable=False, default="tenant", index=True)
    scope_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    credential_kind: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
    public_data: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
    secret_data_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
    secret_keys: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
    allowed_modules: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
    allowed_server_refs: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
    inherit_to_lower_scopes: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
    revision: Mapped[str] = mapped_column(String(36), default=lambda: str(uuid.uuid4()), nullable=False)
    created_by_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
    updated_by_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
    deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
    metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)


@dataclass(frozen=True, slots=True)
class CredentialAccessContext:
    tenant_id: str | None
    user_id: str | None = None
    group_ids: frozenset[str] = frozenset()
    target_scope_type: str = "tenant"
    target_scope_id: str | None = None
    module_id: str | None = None
    server_ref: str | None = None
    administrative: bool = False


@dataclass(frozen=True, slots=True)
class ResolvedCredentialEnvelope:
    id: str
    name: str
    credential_kind: str
    public_data: Mapping[str, Any]
    secret_data: Mapping[str, Any]
    revision: str


def normalize_credential_scope(
    *,
    tenant_id: str | None,
    scope_type: str,
    scope_id: str | None,
) -> tuple[str | None, str, str | None]:
    normalized_type = str(scope_type or "tenant").strip().casefold()
    if normalized_type not in CREDENTIAL_SCOPE_TYPES:
        raise CredentialEnvelopeError(f"Unsupported credential scope: {scope_type!r}")
    normalized_id = _optional_text(scope_id)
    if normalized_type == "system":
        return None, "system", None
    normalized_tenant = _required_text(tenant_id, "Credential tenant_id is required outside system scope")
    if normalized_type == "tenant":
        return normalized_tenant, "tenant", normalized_id or normalized_tenant
    if not normalized_id:
        raise CredentialEnvelopeError(f"{normalized_type.capitalize()} credentials require scope_id")
    return normalized_tenant, normalized_type, normalized_id


def create_credential_envelope(
    session: Session,
    *,
    tenant_id: str | None,
    scope_type: str,
    scope_id: str | None,
    name: str,
    credential_kind: str,
    public_data: Mapping[str, Any] | None = None,
    secret_data: Mapping[str, Any] | None = None,
    description: str | None = None,
    allowed_modules: list[str] | tuple[str, ...] = (),
    allowed_server_refs: list[str] | tuple[str, ...] = (),
    inherit_to_lower_scopes: bool = False,
    is_active: bool = True,
    user_id: str | None = None,
    metadata: Mapping[str, Any] | None = None,
) -> CredentialEnvelope:
    row_tenant_id, row_scope_type, row_scope_id = normalize_credential_scope(
        tenant_id=tenant_id,
        scope_type=scope_type,
        scope_id=scope_id,
    )
    clean_kind = _normalize_kind(credential_kind)
    clean_public = _safe_public_mapping(public_data)
    clean_secret = _json_mapping(secret_data)
    row = CredentialEnvelope(
        tenant_id=row_tenant_id,
        scope_type=row_scope_type,
        scope_id=row_scope_id,
        name=_required_text(name, "Credential name is required"),
        description=_optional_text(description),
        credential_kind=clean_kind,
        public_data=clean_public,
        secret_data_encrypted=_encrypt_secret_mapping(clean_secret),
        secret_keys=sorted(clean_secret),
        allowed_modules=_normalized_values(allowed_modules),
        allowed_server_refs=_normalized_values(allowed_server_refs),
        inherit_to_lower_scopes=bool(inherit_to_lower_scopes),
        is_active=bool(is_active),
        created_by_user_id=_optional_text(user_id),
        updated_by_user_id=_optional_text(user_id),
        metadata_=_json_mapping(metadata) or None,
    )
    session.add(row)
    session.flush()
    _record_credential_change(session, row=row, operation="created", user_id=user_id)
    return row


def update_credential_envelope(
    session: Session,
    row: CredentialEnvelope,
    *,
    name: str | None = None,
    description: str | None = None,
    description_supplied: bool = False,
    credential_kind: str | None = None,
    public_data: Mapping[str, Any] | None = None,
    secret_data: Mapping[str, Any] | None = None,
    clear_secret: bool = False,
    allowed_modules: list[str] | tuple[str, ...] | None = None,
    allowed_server_refs: list[str] | tuple[str, ...] | None = None,
    inherit_to_lower_scopes: bool | None = None,
    is_active: bool | None = None,
    user_id: str | None = None,
    metadata: Mapping[str, Any] | None = None,
) -> CredentialEnvelope:
    if row.deleted_at is not None:
        raise CredentialEnvelopeError("Deleted credentials cannot be updated")
    if name is not None:
        row.name = _required_text(name, "Credential name is required")
    if description_supplied or description is not None:
        row.description = _optional_text(description)
    if credential_kind is not None:
        clean_kind = _normalize_kind(credential_kind)
        if clean_kind != row.credential_kind and secret_data is None and not clear_secret:
            raise CredentialEnvelopeError(
                "Changing credential kind requires replacing or clearing its secret"
            )
        row.credential_kind = clean_kind
    if public_data is not None:
        row.public_data = _safe_public_mapping(public_data)
    if secret_data is not None:
        clean_secret = _json_mapping(secret_data)
        row.secret_data_encrypted = _encrypt_secret_mapping(clean_secret)
        row.secret_keys = sorted(clean_secret)
    elif clear_secret:
        row.secret_data_encrypted = None
        row.secret_keys = []
    if allowed_modules is not None:
        row.allowed_modules = _normalized_values(allowed_modules)
    if allowed_server_refs is not None:
        row.allowed_server_refs = _normalized_values(allowed_server_refs)
    if inherit_to_lower_scopes is not None:
        row.inherit_to_lower_scopes = bool(inherit_to_lower_scopes)
    if is_active is not None:
        row.is_active = bool(is_active)
    if metadata is not None:
        row.metadata_ = _json_mapping(metadata) or None
    row.updated_by_user_id = _optional_text(user_id)
    row.revision = str(uuid.uuid4())
    session.flush()
    _record_credential_change(session, row=row, operation="updated", user_id=user_id)
    return row


def retire_credential_envelope(
    session: Session,
    row: CredentialEnvelope,
    *,
    user_id: str | None = None,
) -> CredentialEnvelope:
    if row.deleted_at is not None:
        return row
    row.secret_data_encrypted = None
    row.secret_keys = []
    row.is_active = False
    row.deleted_at = utcnow()
    row.updated_by_user_id = _optional_text(user_id)
    row.revision = str(uuid.uuid4())
    session.flush()
    _record_credential_change(session, row=row, operation="deleted", user_id=user_id)
    return row


def get_credential_envelope(
    session: Session,
    *,
    credential_id: str,
    context: CredentialAccessContext,
    require_active: bool = True,
    for_update: bool = False,
) -> CredentialEnvelope:
    statement = select(CredentialEnvelope).where(CredentialEnvelope.id == _required_text(credential_id, "Credential id is required"))
    if for_update:
        statement = statement.with_for_update()
    row = session.execute(statement).scalar_one_or_none()
    if row is None or row.deleted_at is not None or not credential_visible_to_context(row, context):
        raise CredentialEnvelopeError("Credential envelope not found")
    if require_active and not row.is_active:
        raise CredentialEnvelopeError("Credential envelope is inactive")
    return row


def list_credential_envelopes(
    session: Session,
    *,
    context: CredentialAccessContext,
    include_inactive: bool = False,
) -> list[CredentialEnvelope]:
    statement = select(CredentialEnvelope).where(CredentialEnvelope.deleted_at.is_(None))
    if context.tenant_id is None:
        statement = statement.where(CredentialEnvelope.tenant_id.is_(None))
    else:
        statement = statement.where(
            (CredentialEnvelope.tenant_id == context.tenant_id)
            | (CredentialEnvelope.tenant_id.is_(None))
        )
    if not include_inactive:
        statement = statement.where(CredentialEnvelope.is_active.is_(True))
    rows = session.execute(statement.order_by(CredentialEnvelope.name, CredentialEnvelope.id)).scalars()
    return [row for row in rows if credential_visible_to_context(row, context)]


def list_managed_credential_envelopes(
    session: Session,
    *,
    tenant_id: str | None,
    include_inactive: bool = False,
) -> list[CredentialEnvelope]:
    """List envelopes for an administrative surface without applying use-site limits."""

    statement = select(CredentialEnvelope).where(CredentialEnvelope.deleted_at.is_(None))
    if tenant_id is None:
        statement = statement.where(CredentialEnvelope.tenant_id.is_(None))
    else:
        statement = statement.where(CredentialEnvelope.tenant_id == tenant_id)
    if not include_inactive:
        statement = statement.where(CredentialEnvelope.is_active.is_(True))
    return list(
        session.execute(
            statement.order_by(
                CredentialEnvelope.scope_type,
                CredentialEnvelope.name,
                CredentialEnvelope.id,
            )
        ).scalars()
    )


def get_managed_credential_envelope(
    session: Session,
    *,
    credential_id: str,
    tenant_id: str | None,
    for_update: bool = False,
) -> CredentialEnvelope:
    statement = select(CredentialEnvelope).where(
        CredentialEnvelope.id
        == _required_text(credential_id, "Credential id is required"),
        CredentialEnvelope.deleted_at.is_(None),
    )
    if tenant_id is None:
        statement = statement.where(CredentialEnvelope.tenant_id.is_(None))
    else:
        statement = statement.where(CredentialEnvelope.tenant_id == tenant_id)
    if for_update:
        statement = statement.with_for_update()
    row = session.execute(statement).scalar_one_or_none()
    if row is None:
        raise CredentialEnvelopeError("Credential envelope not found")
    return row


def credential_visible_to_context(row: CredentialEnvelope, context: CredentialAccessContext) -> bool:
    if row.deleted_at is not None:
        return False
    if not _scope_visible(row, context):
        return False
    if row.allowed_modules and (not context.module_id or context.module_id not in row.allowed_modules):
        return False
    if row.allowed_server_refs and (not context.server_ref or context.server_ref not in row.allowed_server_refs):
        return False
    return True


def resolve_credential_envelope(
    session: Session,
    *,
    credential_id: str,
    context: CredentialAccessContext,
) -> ResolvedCredentialEnvelope:
    row = get_credential_envelope(
        session,
        credential_id=credential_id,
        context=context,
        require_active=True,
    )
    return ResolvedCredentialEnvelope(
        id=row.id,
        name=row.name,
        credential_kind=row.credential_kind,
        public_data=dict(row.public_data or {}),
        secret_data=_decrypt_secret_mapping(row.secret_data_encrypted),
        revision=row.revision,
    )


def credential_envelope_summary(row: CredentialEnvelope) -> dict[str, Any]:
    public_data = redact_secret_values(dict(row.public_data or {}))
    return {
        "id": row.id,
        "tenant_id": row.tenant_id,
        "scope_type": row.scope_type,
        "scope_id": row.scope_id,
        "name": row.name,
        "description": row.description,
        "credential_kind": row.credential_kind,
        "public_data": public_data,
        "secret_keys": sorted(str(key) for key in (row.secret_keys or [])),
        "secret_configured": bool(row.secret_data_encrypted),
        "allowed_modules": sorted(str(item) for item in (row.allowed_modules or [])),
        "allowed_server_refs": sorted(str(item) for item in (row.allowed_server_refs or [])),
        "inherit_to_lower_scopes": bool(row.inherit_to_lower_scopes),
        "is_active": bool(row.is_active),
        "revision": row.revision,
        "created_at": row.created_at,
        "updated_at": row.updated_at,
        "deleted_at": row.deleted_at,
    }


def _scope_visible(row: CredentialEnvelope, context: CredentialAccessContext) -> bool:
    target_type = str(context.target_scope_type or "tenant").strip().casefold()
    target_id = _optional_text(context.target_scope_id)
    if context.administrative:
        return row.scope_type == "system" or row.tenant_id == context.tenant_id
    if row.scope_type == "system":
        return target_type == "system" or bool(row.inherit_to_lower_scopes)
    if row.tenant_id != context.tenant_id:
        return False
    if row.scope_type == "tenant":
        if target_type == "tenant":
            return row.scope_id in {None, context.tenant_id, target_id}
        return bool(row.inherit_to_lower_scopes)
    if row.scope_type == "user":
        return row.scope_id == context.user_id or (target_type == "user" and row.scope_id == target_id)
    if row.scope_type == "group":
        exact = row.scope_id in context.group_ids or (target_type == "group" and row.scope_id == target_id)
        return exact and (target_type == "group" or bool(row.inherit_to_lower_scopes))
    if row.scope_type == "campaign":
        return target_type == "campaign" and row.scope_id == target_id
    return False


def _record_credential_change(
    session: Session,
    *,
    row: CredentialEnvelope,
    operation: str,
    user_id: str | None,
) -> None:
    scope = "system" if row.scope_type == "system" else "tenant"
    details = {
        "scope_type": row.scope_type,
        "scope_id": row.scope_id,
        "credential_kind": row.credential_kind,
        "allowed_modules": list(row.allowed_modules or []),
        "allowed_server_refs": list(row.allowed_server_refs or []),
        "secret_configured": bool(row.secret_data_encrypted),
    }
    record_change(
        session,
        module_id="core",
        collection="core.security.credentials",
        resource_type="credential_envelope",
        resource_id=row.id,
        operation=operation,
        tenant_id=row.tenant_id,
        actor_type="user" if user_id else None,
        actor_id=user_id,
        payload={"scope_type": row.scope_type, "credential_kind": row.credential_kind},
    )
    audit_event(
        session,
        tenant_id=row.tenant_id,
        action=f"credential.{operation}",
        scope=scope,
        user_id=user_id,
        object_type="credential_envelope",
        object_id=row.id,
        details=details,
    )


def _encrypt_secret_mapping(value: Mapping[str, Any]) -> str | None:
    if not value:
        return None
    return encrypt_secret(json.dumps(dict(value), separators=(",", ":"), sort_keys=True))


def _decrypt_secret_mapping(value: str | None) -> dict[str, Any]:
    decrypted = decrypt_secret(value)
    if not decrypted:
        return {}
    try:
        parsed = json.loads(decrypted)
    except json.JSONDecodeError as exc:
        raise CredentialEnvelopeError("Stored credential payload is invalid") from exc
    if not isinstance(parsed, dict):
        raise CredentialEnvelopeError("Stored credential payload is invalid")
    return parsed


def _normalize_kind(value: str) -> str:
    normalized = _required_text(value, "Credential kind is required").casefold()
    if normalized not in CREDENTIAL_KINDS:
        raise CredentialEnvelopeError(f"Unsupported credential kind: {value!r}")
    return normalized


def _normalized_values(values: list[str] | tuple[str, ...]) -> list[str]:
    return sorted({_required_text(value, "Credential policy values cannot be blank") for value in values})


def _json_mapping(value: Mapping[str, Any] | None) -> dict[str, Any]:
    return dict(value or {})


def _safe_public_mapping(value: Mapping[str, Any] | None) -> dict[str, Any]:
    payload = _json_mapping(value)
    unsafe_path = _secret_public_data_path(payload)
    if unsafe_path:
        raise CredentialEnvelopeError(
            f"Credential public_data must not contain secret field {unsafe_path!r}"
        )
    return payload


def _secret_public_data_path(value: object, path: str = "public_data") -> str | None:
    if isinstance(value, Mapping):
        for key, item in value.items():
            item_path = f"{path}.{key}"
            if _is_secret_public_key(key):
                return item_path
            nested_path = _secret_public_data_path(item, item_path)
            if nested_path:
                return nested_path
    elif isinstance(value, list):
        for index, item in enumerate(value):
            nested_path = _secret_public_data_path(item, f"{path}[{index}]")
            if nested_path:
                return nested_path
    return None


def _is_secret_public_key(key: object) -> bool:
    clean_key = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(key).strip())
    clean_key = re.sub(r"[^A-Za-z0-9]+", "_", clean_key).strip("_").casefold()
    if clean_key.endswith(("_id", "_ref", "_reference", "_type")):
        return False
    return clean_key in _SECRET_PUBLIC_DATA_KEYS or is_sensitive_key(key)


def _required_text(value: object | None, message: str) -> str:
    clean = _optional_text(value)
    if not clean:
        raise CredentialEnvelopeError(message)
    return clean


def _optional_text(value: object | None) -> str | None:
    if value is None:
        return None
    clean = str(value).strip()
    return clean or None


__all__ = [
    "CREDENTIAL_KINDS",
    "CREDENTIAL_SCOPE_TYPES",
    "CredentialAccessContext",
    "CredentialEnvelope",
    "CredentialEnvelopeError",
    "ResolvedCredentialEnvelope",
    "create_credential_envelope",
    "credential_envelope_summary",
    "credential_visible_to_context",
    "get_credential_envelope",
    "get_managed_credential_envelope",
    "list_credential_envelopes",
    "list_managed_credential_envelopes",
    "normalize_credential_scope",
    "resolve_credential_envelope",
    "retire_credential_envelope",
    "update_credential_envelope",
]
