"""An in-memory ``CloudProvider`` for tests, dry-runs, and the demo path.

Deterministic (counter-based ids, no randomness or wall-clock), no third-party
deps. It is the reference implementation of the :class:`CloudProvider` Protocol
and the backend the unit + CLI tests run against, so the rest of mgf-cloud needs
no live cloud to test.
"""

from __future__ import annotations

from mgf.cloud.exceptions import ResourceAlreadyExistsError, ResourceNotFoundError
from mgf.cloud.models import (
    OWNER_TAG,
    Flavor,
    FloatingIP,
    Image,
    Keypair,
    Network,
    ObjectContainer,
    ObjectInfo,
    SecurityGroup,
    SecurityRule,
    Server,
    ServerSpec,
    Volume,
    VolumeSnapshot,
    VolumeSpec,
)

__all__ = ["FakeProvider"]


class FakeProvider:
    """In-memory implementation of :class:`CloudProvider`."""

    name = "fake"

    def __init__(self) -> None:
        self._seq = 0
        self._servers: dict[str, Server] = {}
        self._volumes: dict[str, Volume] = {}
        self._volume_snapshots: dict[str, VolumeSnapshot] = {}
        self._floating_ips: dict[str, FloatingIP] = {}  # keyed by address
        self._security_groups: dict[str, SecurityGroup] = {}
        self._keypairs: dict[str, Keypair] = {}
        self._containers: dict[str, dict[str, tuple[bytes, ObjectInfo]]] = {}
        self._flavors: dict[str, Flavor] = {
            f.name: f
            for f in (
                Flavor(
                    id="a2-ram2-disk20", name="a2-ram2-disk20", vcpus=1, ram_mb=2048, disk_gb=20
                ),
                Flavor(
                    id="a2-ram4-disk40", name="a2-ram4-disk40", vcpus=2, ram_mb=4096, disk_gb=40
                ),
            )
        }
        self._images: dict[str, Image] = {
            i.name: i
            for i in (
                Image(id="ubuntu-24.04", name="Ubuntu 24.04 LTS"),
                Image(id="debian-12", name="Debian 12"),
            )
        }
        self._networks: dict[str, Network] = {"ext-net1": Network(id="ext-net1", name="ext-net1")}

    def _next(self, kind: str) -> str:
        self._seq += 1
        return f"{kind}-{self._seq}"

    # --- compute -------------------------------------------------------------
    def list_servers(self, owner: str | None = None) -> list[Server]:
        servers = list(self._servers.values())
        if owner is not None:
            servers = [s for s in servers if s.metadata.get(OWNER_TAG) == owner]
        return servers

    def _server_id(self, key: str) -> str:
        if key in self._servers:
            return key
        for sid, server in self._servers.items():
            if server.name == key:
                return sid
        raise ResourceNotFoundError(key)

    def get_server(self, name_or_id: str) -> Server:
        return self._servers[self._server_id(name_or_id)]

    def create_server(self, spec: ServerSpec) -> Server:
        if any(s.name == spec.name for s in self._servers.values()):
            raise ResourceAlreadyExistsError(spec.name)
        sid = self._next("server")
        floating: tuple[str, ...] = ()
        if spec.assign_floating_ip:
            fip = self.create_floating_ip(spec.network)
            floating = (fip.address,)
        if spec.volume_size_gb is not None:
            boot = self.create_volume(
                VolumeSpec(name=f"{spec.name}-boot", size_gb=spec.volume_size_gb, bootable=True)
            )
            self._volumes[boot.id] = boot.model_copy(
                update={"attached_server_id": sid, "status": "in-use"}
            )
        server = Server(
            id=sid,
            name=spec.name,
            status="ACTIVE",
            flavor=spec.flavor,
            image=spec.image,
            key_name=spec.key_name,
            private_ips=(f"10.0.0.{self._seq}",),
            floating_ips=floating,
            security_groups=spec.security_groups,
            metadata=dict(spec.metadata),
            created_at="2026-01-01T00:00:00Z",
        )
        self._servers[sid] = server
        if floating:
            addr = floating[0]
            self._floating_ips[addr] = self._floating_ips[addr].model_copy(
                update={"attached_server_id": sid}
            )
        return server

    def delete_server(self, name_or_id: str) -> None:
        sid = self._server_id(name_or_id)
        for addr, fip in list(self._floating_ips.items()):
            if fip.attached_server_id == sid:
                self._floating_ips[addr] = fip.model_copy(update={"attached_server_id": None})
        for vid, vol in list(self._volumes.items()):
            if vol.attached_server_id == sid:
                self._volumes[vid] = vol.model_copy(
                    update={"attached_server_id": None, "status": "available"}
                )
        del self._servers[sid]

    def reboot_server(self, name_or_id: str, *, hard: bool = False) -> None:
        self._server_id(name_or_id)  # existence check

    def resize_server(self, name_or_id: str, flavor: str) -> None:
        sid = self._server_id(name_or_id)
        self._servers[sid] = self._servers[sid].model_copy(update={"flavor": flavor})

    def snapshot_server(self, name_or_id: str, snapshot_name: str) -> Image:
        self._server_id(name_or_id)
        image = Image(id=self._next("image"), name=snapshot_name)
        self._images[image.name] = image
        return image

    def list_flavors(self) -> list[Flavor]:
        return list(self._flavors.values())

    def list_images(self) -> list[Image]:
        return list(self._images.values())

    # --- keypairs ------------------------------------------------------------
    def list_keypairs(self) -> list[Keypair]:
        return list(self._keypairs.values())

    def create_keypair(self, name: str, public_key: str) -> Keypair:
        if name in self._keypairs:
            raise ResourceAlreadyExistsError(name)
        keypair = Keypair(name=name, fingerprint=f"fake:{len(public_key)}", public_key=public_key)
        self._keypairs[name] = keypair
        return keypair

    def delete_keypair(self, name: str) -> None:
        if name not in self._keypairs:
            raise ResourceNotFoundError(name)
        del self._keypairs[name]

    # --- block storage -------------------------------------------------------
    def list_volumes(self) -> list[Volume]:
        return list(self._volumes.values())

    def _volume_id(self, key: str) -> str:
        if key in self._volumes:
            return key
        for vid, vol in self._volumes.items():
            if vol.name == key:
                return vid
        raise ResourceNotFoundError(key)

    def get_volume(self, name_or_id: str) -> Volume:
        return self._volumes[self._volume_id(name_or_id)]

    def create_volume(self, spec: VolumeSpec) -> Volume:
        # EH-12: mirror the infomaniak adapter's sibling guard so a retry can't
        # leak a duplicate billable volume.
        if any(v.name == spec.name for v in self._volumes.values()):
            raise ResourceAlreadyExistsError(spec.name)
        vid = self._next("volume")
        volume = Volume(id=vid, name=spec.name, size_gb=spec.size_gb, bootable=spec.bootable)
        self._volumes[vid] = volume
        return volume

    def attach_volume(self, volume: str, server: str) -> None:
        vid = self._volume_id(volume)
        sid = self._server_id(server)
        self._volumes[vid] = self._volumes[vid].model_copy(
            update={"attached_server_id": sid, "status": "in-use"}
        )

    def detach_volume(self, volume: str, server: str) -> None:
        vid = self._volume_id(volume)
        self._server_id(server)
        self._volumes[vid] = self._volumes[vid].model_copy(
            update={"attached_server_id": None, "status": "available"}
        )

    def delete_volume(self, volume: str) -> None:
        del self._volumes[self._volume_id(volume)]

    def snapshot_volume(self, volume: str, snapshot_name: str) -> VolumeSnapshot:
        vid = self._volume_id(volume)
        snapshot = VolumeSnapshot(
            id=self._next("snap"),
            name=snapshot_name,
            volume_id=vid,
            size_gb=self._volumes[vid].size_gb,
        )
        self._volume_snapshots[snapshot.id] = snapshot
        return snapshot

    # --- network -------------------------------------------------------------
    def list_networks(self) -> list[Network]:
        return list(self._networks.values())

    def list_floating_ips(self) -> list[FloatingIP]:
        return list(self._floating_ips.values())

    def create_floating_ip(self, network: str | None = None) -> FloatingIP:
        fip = FloatingIP(id=self._next("fip"), address=f"203.0.113.{self._seq}")
        self._floating_ips[fip.address] = fip
        return fip

    def attach_floating_ip(self, address: str, server: str) -> None:
        if address not in self._floating_ips:
            raise ResourceNotFoundError(address)
        sid = self._server_id(server)
        self._floating_ips[address] = self._floating_ips[address].model_copy(
            update={"attached_server_id": sid}
        )
        server_obj = self._servers[sid]
        if address not in server_obj.floating_ips:
            self._servers[sid] = server_obj.model_copy(
                update={"floating_ips": (*server_obj.floating_ips, address)}
            )

    def detach_floating_ip(self, address: str) -> None:
        if address not in self._floating_ips:
            raise ResourceNotFoundError(address)
        fip = self._floating_ips[address]
        if fip.attached_server_id and fip.attached_server_id in self._servers:
            server_obj = self._servers[fip.attached_server_id]
            self._servers[fip.attached_server_id] = server_obj.model_copy(
                update={"floating_ips": tuple(a for a in server_obj.floating_ips if a != address)}
            )
        self._floating_ips[address] = fip.model_copy(update={"attached_server_id": None})

    def release_floating_ip(self, address: str) -> None:
        if address not in self._floating_ips:
            raise ResourceNotFoundError(address)
        del self._floating_ips[address]

    def list_security_groups(self) -> list[SecurityGroup]:
        return list(self._security_groups.values())

    def _security_group_id(self, key: str) -> str:
        if key in self._security_groups:
            return key
        for gid, group in self._security_groups.items():
            if group.name == key:
                return gid
        raise ResourceNotFoundError(key)

    def create_security_group(
        self,
        name: str,
        description: str = "",
        rules: tuple[SecurityRule, ...] = (),
    ) -> SecurityGroup:
        if any(g.name == name for g in self._security_groups.values()):
            raise ResourceAlreadyExistsError(name)
        group = SecurityGroup(
            id=self._next("secgroup"), name=name, description=description, rules=tuple(rules)
        )
        self._security_groups[group.id] = group
        return group

    def delete_security_group(self, name_or_id: str) -> None:
        del self._security_groups[self._security_group_id(name_or_id)]

    # --- object storage ------------------------------------------------------
    def list_containers(self) -> list[ObjectContainer]:
        return [
            ObjectContainer(
                name=name,
                object_count=len(objects),
                bytes=sum(len(data) for data, _ in objects.values()),
            )
            for name, objects in self._containers.items()
        ]

    def create_container(self, name: str) -> ObjectContainer:
        if name in self._containers:
            raise ResourceAlreadyExistsError(name)
        self._containers[name] = {}
        return ObjectContainer(name=name)

    def delete_container(self, name: str) -> None:
        if name not in self._containers:
            raise ResourceNotFoundError(name)
        del self._containers[name]

    def _container(self, name: str) -> dict[str, tuple[bytes, ObjectInfo]]:
        if name not in self._containers:
            raise ResourceNotFoundError(name)
        return self._containers[name]

    def list_objects(self, container: str) -> list[ObjectInfo]:
        return [info for _, info in self._container(container).values()]

    def put_object(
        self,
        container: str,
        name: str,
        data: bytes,
        content_type: str = "application/octet-stream",
    ) -> ObjectInfo:
        objects = self._container(container)
        info = ObjectInfo(
            name=name, bytes=len(data), content_type=content_type, etag=f"fake-{len(data)}"
        )
        objects[name] = (data, info)
        return info

    def get_object(self, container: str, name: str) -> bytes:
        objects = self._container(container)
        if name not in objects:
            raise ResourceNotFoundError(name)
        return objects[name][0]

    def delete_object(self, container: str, name: str) -> None:
        objects = self._container(container)
        if name not in objects:
            raise ResourceNotFoundError(name)
        del objects[name]
