from __future__ import annotations

import os
from typing import TYPE_CHECKING, Any, Generic

from eventsourcing.dcb.domain import (
    EnduringObject,
    Initialises,
    Perspective,
    Selector,
    TGroup,
    TMutates,
    TPerspective,
)
from eventsourcing.dcb.persistence import (
    DCBEventStore,
    DCBInfrastructureFactory,
    NotFoundError,
)
from eventsourcing.utils import Environment, EnvType, resolve_topic

if TYPE_CHECKING:
    from collections.abc import Mapping
    from types import TracebackType

    from typing_extensions import Self


class DCBApplication:
    name = "DCBApplication"
    env: Mapping[str, str] = {"PERSISTENCE_MODULE": "eventsourcing.dcb.popo"}

    def __init_subclass__(cls, **kwargs: Any) -> None:
        if "name" not in cls.__dict__:
            cls.name = cls.__name__

    def __init__(self, env: EnvType | None = None):
        self.env = self.construct_env(self.name, env)
        self.factory = DCBInfrastructureFactory.construct(self.env)
        self.recorder = self.factory.dcb_event_store()
        if "MAPPER_TOPIC" in self.env:
            mapper = resolve_topic(self.env["MAPPER_TOPIC"])()
            self.events = DCBEventStore(mapper, self.recorder)
            self.repository = DCBRepository(self.events)

    def construct_env(self, name: str, env: EnvType | None = None) -> Environment:
        """Constructs environment from which application will be configured."""
        _env = dict(type(self).env)
        _env.update(os.environ)
        if env is not None:
            _env.update(env)
        return Environment(name, _env)

    def __enter__(self) -> Self:
        self.factory.__enter__()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        self.factory.__exit__(exc_type, exc_val, exc_tb)


class DCBRepository(Generic[TMutates]):
    def __init__(self, eventstore: DCBEventStore[TMutates]):
        self.eventstore = eventstore

    def save(self, p: Perspective[TMutates]) -> int:
        return self.eventstore.put(
            events=p.collect_events(),
            cb=p.cb,
            after=p.last_known_position,
        )

    def get(
        self,
        enduring_object_id: str,
    ) -> EnduringObject[TMutates]:
        cb = [Selector(tags=[enduring_object_id])]
        events = self.eventstore.get(*cb)
        obj: EnduringObject[TMutates] | None = None
        for event in events:
            obj = event.mutates.mutate(obj)
        if obj is None:
            raise NotFoundError
        obj.last_known_position = events.head
        return obj

    def get_many(
        self,
        *enduring_object_ids: str,
    ) -> list[EnduringObject[TMutates] | None]:
        cb = [
            Selector(tags=[enduring_object_id])
            for enduring_object_id in enduring_object_ids
        ]
        events = self.eventstore.get(cb)
        objs: dict[str, EnduringObject[TMutates] | None] = dict.fromkeys(
            enduring_object_ids
        )
        for event in events:
            for tag in event.tags:
                obj = objs.get(tag)
                if not isinstance(event.mutates, Initialises) and not obj:
                    continue
                obj = event.mutates.mutate(obj)
                objs[tag] = obj
        for obj in objs.values():
            if obj is not None:
                obj.last_known_position = events.head
        return list(objs.values())

    def get_group(self, cls: type[TGroup], *enduring_object_ids: str) -> TGroup:
        enduring_objects = self.get_many(*enduring_object_ids)
        perspective = cls(*enduring_objects)
        last_known_positions = [
            o.last_known_position
            for o in enduring_objects
            if o and o.last_known_position
        ]
        perspective.last_known_position = (
            max(last_known_positions) if last_known_positions else None
        )
        return perspective

    def project_perspective(self, p: TPerspective) -> TPerspective:
        events = self.eventstore.get(p.cb)
        for event in events:
            event.mutates.mutate(p)
        p.last_known_position = events.head
        return p
