import asyncio
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from csirtmu.core.temporal.s3 import S3Config
from minio import Minio
from minio.datatypes import Object as MinioObject
from sqlalchemy import URL, delete
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from csirtmu.perun_sync.perun_sync import models
from csirtmu.perun_sync.perun_sync.dataclasses_perun import PerunPropagation
from csirtmu.perun_sync.perun_sync.s3_data import load_perun_data_from_s3
from csirtmu.perun_sync.perun_sync.types import PerunDatabaseSyncResult
from csirtmu.perun_sync.perun_sync.utilities import to_async_iterable

if TYPE_CHECKING:
    from collections.abc import Iterator


async def s3_listen_for_new_object(config: S3Config, timeout: int) -> str | None:  # noqa: ASYNC109
    try:
        async with asyncio.timeout(timeout):
            return await do_s3_listen_for_new_object(config)
    except TimeoutError:
        return None


async def do_s3_listen_for_new_object(config: S3Config) -> str | None:
    client: Minio = Minio(
        config.host,
        access_key=config.access_key,
        secret_key=config.secret_key,
        cert_check=False,
        secure=config.secure,
    )

    with client.listen_bucket_notification(
        config.bucket, config.prefix, ".json*", events=("s3:ObjectCreated:*",)
    ) as events:
        async for event in to_async_iterable(events):
            if r := event["Records"]:
                return r[0]["s3"]["object"]["key"]

    return None


def lambda_last_modified(x: MinioObject) -> datetime:
    modified: datetime | None = x.last_modified

    if not modified:
        modified = datetime.min.replace(tzinfo=UTC)

    return modified


# Find the latest object in prefix. Useful for CRON-like periodic checks.
def s3_get_latest_object(config: S3Config) -> str | None:
    client: Minio = Minio(
        config.host,
        access_key=config.access_key,
        secret_key=config.secret_key,
        cert_check=False,
        secure=config.secure,
    )

    object_list: Iterator[MinioObject] = client.list_objects(config.bucket, config.prefix)
    latest_object: MinioObject = max(object_list, key=lambda_last_modified)

    return latest_object.object_name


def _s3_get_perun_data(config: S3Config, s3_key: str) -> PerunPropagation:
    raw_data = load_perun_data_from_s3(config, s3_key)
    return PerunPropagation.from_json(raw_data)


async def sync_to_database(s3_config: S3Config, db_url: URL, s3_key: str, dry_run: bool) -> PerunDatabaseSyncResult:
    perun_data: PerunPropagation = _s3_get_perun_data(s3_config, s3_key)
    engine = create_async_engine(db_url)
    async with engine.begin() as conn, AsyncSession(conn) as session:
        deleted = (
            await session.scalars(
                delete(models.PerunUser)
                .where(models.PerunUser.facility == perun_data.metadata.facility.uuid)
                .returning(models.PerunUser.id)
            )
        ).all()
        await session.flush()
        for user in perun_data.users:
            propagation = models.PerunUser(
                perun_id=user.id,
                display_name=user.display_name,
                preferred_mail=user.preferred_mail,
                login_mu=user.login_mu,
                scoped_login_mu=user.scoped_login_mu,
                aliases=user.aliases,
                facility=perun_data.metadata.facility.uuid,
            )
            session.add(propagation)
        if dry_run:
            await session.rollback()
            result = PerunDatabaseSyncResult(
                facility=perun_data.metadata.facility.uuid,
                deleted=deleted,
                inserted=len(perun_data.users),
                dry_run=True,
            )
        else:
            await session.commit()
            result = PerunDatabaseSyncResult(
                facility=perun_data.metadata.facility.uuid, deleted=deleted, inserted=len(perun_data.users)
            )
    await engine.dispose()
    return result
