from __future__ import annotations

from dataclasses import dataclass
import json
import os
import re
import secrets
from typing import Any

from .models import MinIOSettings


@dataclass(slots=True)
class MinIOProvisioningConfig:
    enabled: bool
    endpoint: str
    public_endpoint: str
    admin_access_key: str
    admin_secret_key: str
    bucket: str
    secure: bool
    region: str | None


class MinIOProvisioningError(RuntimeError):
    pass


class MinIODeprovisioningError(RuntimeError):
    pass


def minio_provisioning_config_from_env() -> MinIOProvisioningConfig:
    endpoint = _env("USER_FILES_MINIO_ENDPOINT") or _env("MINIO_ENDPOINT")
    public_endpoint = _env("USER_FILES_MINIO_PUBLIC_ENDPOINT") or endpoint
    return MinIOProvisioningConfig(
        enabled=_truthy(_env("USER_FILES_MINIO_PROVISIONING_ENABLED")),
        endpoint=endpoint,
        public_endpoint=public_endpoint,
        admin_access_key=_env("USER_FILES_MINIO_ADMIN_ACCESS_KEY") or _env("MINIO_ROOT_USER"),
        admin_secret_key=_env("USER_FILES_MINIO_ADMIN_SECRET_KEY") or _env("MINIO_ROOT_PASSWORD"),
        bucket=_env("USER_FILES_MINIO_BUCKET") or "beaver-user-files",
        secure=_truthy(_env("USER_FILES_MINIO_SECURE")),
        region=_env("USER_FILES_MINIO_REGION") or None,
    )


def provision_user_file_minio_settings(
    *,
    backend_id: str,
    existing: MinIOSettings | None,
    config: MinIOProvisioningConfig | None = None,
) -> MinIOSettings | None:
    cfg = config or minio_provisioning_config_from_env()
    if not cfg.enabled:
        return existing
    if existing is not None and existing.configured and existing.secret_key:
        return existing
    if not cfg.endpoint or not cfg.admin_access_key or not cfg.admin_secret_key:
        raise MinIOProvisioningError("MinIO provisioning requires endpoint and admin credentials")

    namespace = default_namespace(backend_id)
    access_key = _access_key_for_backend(backend_id)
    secret_key = secrets.token_urlsafe(32)
    policy_name = _policy_name_for_backend(backend_id)

    try:
        from minio import Minio
        from minio import MinioAdmin
        from minio.credentials import StaticProvider

        client = Minio(
            endpoint=cfg.endpoint,
            access_key=cfg.admin_access_key,
            secret_key=cfg.admin_secret_key,
            secure=cfg.secure,
            region=cfg.region,
        )
        if not client.bucket_exists(cfg.bucket):
            client.make_bucket(cfg.bucket, location=cfg.region)

        admin = MinioAdmin(
            endpoint=cfg.endpoint,
            credentials=StaticProvider(
                access_key=cfg.admin_access_key,
                secret_key=cfg.admin_secret_key,
            ),
            secure=cfg.secure,
        )
        try:
            admin.user_add(access_key, secret_key)
        except Exception:
            # Treat an existing user as idempotent; keep the newly generated
            # secret only when user creation succeeded.
            if existing is not None and existing.secret_key:
                secret_key = existing.secret_key
            else:
                raise

        policy = _namespace_policy(bucket=cfg.bucket, namespace=namespace)
        admin.policy_add(policy_name, policy=policy)
        try:
            admin.attach_policy(policies=[policy_name], user=access_key)
        except Exception as exc:
            if not _is_policy_attach_already_applied(exc):
                raise
    except Exception as exc:
        raise MinIOProvisioningError(f"MinIO user file provisioning failed: {exc}") from exc

    return MinIOSettings(
        endpoint=cfg.public_endpoint,
        access_key=access_key,
        secret_key=secret_key,
        bucket=cfg.bucket,
        namespace=namespace,
        secure=cfg.secure,
        region=cfg.region,
    )


def deprovision_user_file_minio_resources(
    *,
    backend_id: str,
    existing: MinIOSettings | None,
    best_effort: bool = False,
    config: MinIOProvisioningConfig | None = None,
) -> dict[str, Any]:
    cfg = config or minio_provisioning_config_from_env()
    settings_found = existing is not None
    result: dict[str, Any] = {
        "ok": True,
        "backend_id": backend_id,
        "settings_found": settings_found,
        "best_effort": best_effort,
        "bucket": "skipped",
        "objects": {"status": "absent", "deleted": 0},
        "user": {"status": "absent"},
        "policy": {"status": "absent"},
    }

    if existing is None and not best_effort:
        return result

    bucket = (existing.bucket if existing else None) or cfg.bucket
    namespace = (existing.namespace if existing else None) or default_namespace(backend_id)
    access_key = (existing.access_key if existing else None) or _access_key_for_backend(backend_id)
    policy_name = _policy_name_for_backend(backend_id)

    if not bucket or not namespace.strip("/"):
        raise MinIODeprovisioningError("MinIO deprovisioning requires bucket and namespace")
    if not cfg.endpoint or not cfg.admin_access_key or not cfg.admin_secret_key:
        raise MinIODeprovisioningError("MinIO deprovisioning requires endpoint and admin credentials")

    try:
        from minio import Minio
        from minio import MinioAdmin
        from minio.credentials import StaticProvider
        from minio.deleteobjects import DeleteObject

        client = Minio(
            endpoint=cfg.endpoint,
            access_key=cfg.admin_access_key,
            secret_key=cfg.admin_secret_key,
            secure=cfg.secure,
            region=cfg.region,
        )
        admin = MinioAdmin(
            endpoint=cfg.endpoint,
            credentials=StaticProvider(
                access_key=cfg.admin_access_key,
                secret_key=cfg.admin_secret_key,
            ),
            secure=cfg.secure,
        )

        prefix = namespace.strip("/")
        if client.bucket_exists(bucket):
            result["bucket"] = "present"
            objects = [
                DeleteObject(item.object_name)
                for item in client.list_objects(bucket, prefix=f"{prefix}/", recursive=True)
                if getattr(item, "object_name", None)
            ]
            errors = list(client.remove_objects(bucket, objects)) if objects else []
            if errors:
                result["objects"] = {
                    "status": "failed",
                    "deleted": 0,
                    "errors": [_safe_error_text(error) for error in errors],
                }
                result["ok"] = False
            else:
                result["objects"] = {
                    "status": "removed" if objects else "absent",
                    "deleted": len(objects),
                }
        else:
            result["bucket"] = "absent"
            result["objects"] = {"status": "absent", "deleted": 0}

        result["policy_detach"] = _admin_step(
            lambda: _call_admin_method(
                admin,
                ["detach_policy"],
                policies=[policy_name],
                user=access_key,
            )
        )
        result["user"] = _admin_step(lambda: _call_admin_method(admin, ["user_remove", "remove_user"], access_key))
        result["policy"] = _admin_step(
            lambda: _call_admin_method(admin, ["policy_remove", "remove_policy"], policy_name)
        )
    except Exception as exc:
        raise MinIODeprovisioningError(f"MinIO user file deprovisioning failed: {_safe_error_text(exc)}") from exc

    result["ok"] = bool(result["ok"]) and all(
        item.get("status") != "failed"
        for item in (
            result["objects"],
            result["policy_detach"],
            result["user"],
            result["policy"],
        )
        if isinstance(item, dict)
    )
    return result


def default_namespace(backend_id: str) -> str:
    return f"users/{backend_id.strip().strip('/')}"


def _namespace_policy(*, bucket: str, namespace: str) -> dict[str, object]:
    prefix = namespace.strip("/")
    return {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": ["s3:GetBucketLocation"],
                "Resource": [f"arn:aws:s3:::{bucket}"],
            },
            {
                "Effect": "Allow",
                "Action": ["s3:ListBucket"],
                "Resource": [f"arn:aws:s3:::{bucket}"],
                "Condition": {"StringLike": {"s3:prefix": [f"{prefix}/*", prefix]}},
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject",
                    "s3:PutObject",
                    "s3:DeleteObject",
                ],
                "Resource": [f"arn:aws:s3:::{bucket}/{prefix}/*"],
            },
        ],
    }


def policy_json_for_backend(*, backend_id: str, bucket: str = "beaver-user-files") -> str:
    return json.dumps(_namespace_policy(bucket=bucket, namespace=default_namespace(backend_id)), indent=2)


def _access_key_for_backend(backend_id: str) -> str:
    slug = re.sub(r"[^A-Za-z0-9_-]+", "-", backend_id.strip()).strip("-_")
    if not slug:
        slug = secrets.token_hex(6)
    return f"beaver-{slug}"[:64]


def _policy_name_for_backend(backend_id: str) -> str:
    slug = re.sub(r"[^A-Za-z0-9_-]+", "-", backend_id.strip()).strip("-_")
    return f"beaver-user-files-{slug or secrets.token_hex(6)}"[:128]


def _call_admin_method(admin: object, names: list[str], *args: Any, **kwargs: Any) -> Any:
    for name in names:
        method = getattr(admin, name, None)
        if callable(method):
            return method(*args, **kwargs)
    raise AttributeError(f"MinIO admin client does not support any of: {', '.join(names)}")


def _admin_step(action: Any) -> dict[str, Any]:
    try:
        action()
    except Exception as exc:
        if _is_absent_error(exc):
            return {"status": "absent"}
        return {"status": "failed", "error": _safe_error_text(exc)}
    return {"status": "removed"}


def _is_absent_error(exc: Exception) -> bool:
    text = _safe_error_text(exc).lower()
    absent_markers = (
        "not found",
        "notfound",
        "no such",
        "does not exist",
        "doesn't exist",
        "specified user does not exist",
        "specified policy does not exist",
        "the specified bucket does not exist",
    )
    return any(marker in text for marker in absent_markers)


def _is_policy_attach_already_applied(exc: Exception) -> bool:
    text = _safe_error_text(exc)
    return (
        "XMinioAdminPolicyChangeAlreadyApplied" in text
        or "specified policy change is already in effect" in text.lower()
        or "policy update has no net effect" in text.lower()
    )


def _safe_error_text(exc: object) -> str:
    text = str(exc).strip()
    return text or exc.__class__.__name__


def _env(name: str) -> str:
    return os.getenv(name, "").strip()


def _truthy(value: str) -> bool:
    return value.lower() in {"1", "true", "yes", "on"}
