"""Client S3 pour le bucket datalake avec préfixage automatique par environnement."""

from __future__ import annotations

import io
import json
from typing import TYPE_CHECKING

from minio import Minio
from minio.error import S3Error
from pydantic import BaseModel

from .exceptions import MinIOError

if TYPE_CHECKING:
    from minio.datatypes import Object

MIME_TYPES: dict[str, str] = {
    ".png": "image/png",
    ".pdf": "application/pdf",
    ".json": "application/json",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".tiff": "image/tiff",
    ".tif": "image/tiff",
}


class DatalakeClient:
    """Client S3 pour le bucket datalake avec préfixage par environnement."""

    def __init__(
        self,
        endpoint: str,
        access_key: str,
        secret_key: str,
        bucket: str,
        env_prefix: str = "dev",
        secure: bool = False,
    ) -> None:
        """Initialise le client datalake.

        Args:
            endpoint: Endpoint S3 (ex: "s3.sbg.io.cloud.ovh.net").
            access_key: Clé d'accès.
            secret_key: Clé secrète.
            bucket: Nom du bucket datalake.
            env_prefix: Préfixe d'environnement (dev, preprod, prod).
            secure: Utiliser HTTPS si True.
        """
        self._bucket = bucket
        self._env_prefix = env_prefix
        try:
            self._client = Minio(
                endpoint=endpoint,
                access_key=access_key,
                secret_key=secret_key,
                secure=secure,
            )
        except Exception as e:
            raise MinIOError(f"Échec de connexion au datalake: {e}") from e

    def _full_path(self, path: str) -> str:
        """Préfixe un chemin avec l'environnement.

        Args:
            path: Chemin relatif de l'objet.

        Returns:
            Chemin complet avec préfixe environnement.
        """
        return f"{self._env_prefix}/{path}"

    def put_json(self, path: str, data: dict | BaseModel) -> None:
        """Écrit un JSON dans le datalake.

        Args:
            path: Chemin de l'objet (sans env_prefix).
            data: Données à sérialiser (dict ou Pydantic model).

        Raises:
            MinIOError: Si l'écriture échoue.
        """
        full = self._full_path(path)
        try:
            if isinstance(data, BaseModel):
                json_bytes = data.model_dump_json().encode("utf-8")
            else:
                json_bytes = json.dumps(data, ensure_ascii=False).encode("utf-8")

            data_stream = io.BytesIO(json_bytes)
            self._client.put_object(
                bucket_name=self._bucket,
                object_name=full,
                data=data_stream,
                length=len(json_bytes),
                content_type="application/json",
            )
        except S3Error as e:
            raise MinIOError(f"Échec d'écriture sur le datalake ({path}): {e}") from e
        except Exception as e:
            raise MinIOError(f"Erreur lors de l'écriture JSON ({path}): {e}") from e

    def get_json(self, path: str) -> dict:
        """Lit un JSON depuis le datalake.

        Args:
            path: Chemin de l'objet (sans env_prefix).

        Returns:
            Données désérialisées.

        Raises:
            MinIOError: Si la lecture échoue.
        """
        full = self._full_path(path)
        try:
            response = self._client.get_object(
                bucket_name=self._bucket,
                object_name=full,
            )
            try:
                data = json.loads(response.read().decode("utf-8"))
                return data
            finally:
                response.close()
                response.release_conn()
        except S3Error as e:
            raise MinIOError(f"Échec de lecture sur le datalake ({path}): {e}") from e
        except json.JSONDecodeError as e:
            raise MinIOError(f"JSON invalide sur le datalake ({path}): {e}") from e
        except Exception as e:
            raise MinIOError(f"Erreur lors de la lecture JSON ({path}): {e}") from e

    def put_bytes(
        self, path: str, data: bytes, content_type: str | None = None
    ) -> None:
        """Écrit des données binaires dans le datalake.

        Args:
            path: Chemin de l'objet (sans env_prefix).
            data: Contenu binaire.
            content_type: Type MIME. Déduit de l'extension si absent.

        Raises:
            MinIOError: Si l'écriture échoue.
        """
        full = self._full_path(path)
        if content_type is None:
            content_type = _infer_content_type(path)
        try:
            data_stream = io.BytesIO(data)
            self._client.put_object(
                bucket_name=self._bucket,
                object_name=full,
                data=data_stream,
                length=len(data),
                content_type=content_type,
            )
        except S3Error as e:
            raise MinIOError(f"Échec d'écriture sur le datalake ({path}): {e}") from e
        except Exception as e:
            raise MinIOError(f"Erreur lors de l'écriture binaire ({path}): {e}") from e

    def get_bytes(self, path: str) -> bytes:
        """Lit des données binaires depuis le datalake.

        Args:
            path: Chemin de l'objet (sans env_prefix).

        Returns:
            Contenu binaire de l'objet.

        Raises:
            MinIOError: Si la lecture échoue.
        """
        full = self._full_path(path)
        try:
            response = self._client.get_object(
                bucket_name=self._bucket,
                object_name=full,
            )
            try:
                return response.read()
            finally:
                response.close()
                response.release_conn()
        except S3Error as e:
            raise MinIOError(f"Échec de lecture sur le datalake ({path}): {e}") from e
        except Exception as e:
            raise MinIOError(f"Erreur lors de la lecture binaire ({path}): {e}") from e

    def exists(self, path: str) -> bool:
        """Vérifie l'existence d'un objet dans le datalake.

        Args:
            path: Chemin de l'objet (sans env_prefix).

        Returns:
            True si l'objet existe, False sinon.
        """
        full = self._full_path(path)
        try:
            self._client.stat_object(
                bucket_name=self._bucket,
                object_name=full,
            )
            return True
        except S3Error as e:
            if e.code == "NoSuchKey":
                return False
            raise MinIOError(
                f"Échec de vérification sur le datalake ({path}): {e}"
            ) from e
        except Exception as e:
            raise MinIOError(
                f"Erreur lors de la vérification ({path}): {e}"
            ) from e

    def list_prefix(self, prefix: str) -> list[str]:
        """Liste les objets avec un préfixe donné.

        Args:
            prefix: Préfixe pour filtrer (sans env_prefix).

        Returns:
            Liste des chemins d'objets (sans env_prefix).
        """
        full = self._full_path(prefix)
        env_prefix_len = len(self._env_prefix) + 1  # +1 pour le "/"
        try:
            objects: list[Object] = list(
                self._client.list_objects(
                    bucket_name=self._bucket,
                    prefix=full,
                    recursive=True,
                )
            )
            return [
                obj.object_name[env_prefix_len:]
                for obj in objects
                if obj.object_name
            ]
        except S3Error as e:
            raise MinIOError(
                f"Échec de listage sur le datalake ({prefix}): {e}"
            ) from e
        except Exception as e:
            raise MinIOError(f"Erreur lors du listage ({prefix}): {e}") from e


def _infer_content_type(path: str) -> str:
    """Déduit le type MIME depuis l'extension du fichier.

    Args:
        path: Chemin du fichier.

    Returns:
        Type MIME correspondant à l'extension, ou application/octet-stream.
    """
    dot_idx = path.rfind(".")
    if dot_idx == -1:
        return "application/octet-stream"
    ext = path[dot_idx:].lower()
    return MIME_TYPES.get(ext, "application/octet-stream")
