"""Storage semantic handlers."""

from __future__ import annotations

import json
from typing import Any

from fastapi import Request

from app.api.errors import ApiError
from app.api.registry import HandlerRegistry
from app.handlers.common import database, require_node, state, storage_payload, subdirs, values
from app.simulation.seed import CLUSTER_ID, stable_id


def register_storage_handlers(registry: HandlerRegistry) -> None:
    async def _storage_row(request: Request, node: str | None, storage_id: str) -> Any:
        row = await database(request).pool.fetchrow(
            """SELECT s.storage_id, s.storage_type, s.shared, s.capacity_bytes, s.used_bytes,
                s.config, n.name AS node_name
            FROM storages s
            JOIN resources r ON r.id = s.resource_id
            JOIN nodes n ON n.id = r.node_id
            WHERE s.storage_id=$1 AND ($2::text IS NULL OR n.name=$2)""",
            storage_id,
            node,
        )
        if row is None:
            raise ApiError(404, "storage does not exist")
        return row

    async def storage_ids(_request: Request, _inputs: dict[str, Any]) -> list[dict[str, str]]:
        rows = await database(_request).pool.fetch(
            "SELECT DISTINCT storage_id FROM storages ORDER BY storage_id"
        )
        return [{"storage": str(row["storage_id"])} for row in rows]

    async def storage_create(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        storage_id = str(payload["storage"])
        storage_type = str(payload.get("type") or "dir")
        exists = await database(request).pool.fetchval(
            "SELECT EXISTS(SELECT 1 FROM storages WHERE storage_id=$1)",
            storage_id,
        )
        if exists:
            raise ApiError(409, "storage ID already exists")
        node = await database(request).pool.fetchrow(
            "SELECT id, name FROM nodes ORDER BY name LIMIT 1"
        )
        if node is None:
            raise ApiError(503, "no nodes available")
        resource_id = stable_id(f"storage:{storage_id}")
        config = {
            key: value
            for key, value in payload.items()
            if key not in {"storage", "type", "nodes", "delete"}
        }
        if "content" in payload:
            config["content"] = [
                item.strip() for item in str(payload["content"]).split(",") if item.strip()
            ]
        async with database(request).pool.acquire() as connection:
            async with connection.transaction():
                await connection.execute(
                    """INSERT INTO resources(id, node_id, kind, external_id, state, cluster_id)
                    VALUES($1, $2, 'storage', $3, $4::jsonb, $5)""",
                    resource_id,
                    node["id"],
                    storage_id,
                    json.dumps({**config, "status": "available"}, sort_keys=True),
                    CLUSTER_ID,
                )
                await connection.execute(
                    """INSERT INTO storages(
                        resource_id, cluster_id, storage_id, storage_type, shared, config
                    ) VALUES($1, $2, $3, $4, $5, $6::jsonb)""",
                    resource_id,
                    CLUSTER_ID,
                    storage_id,
                    storage_type,
                    bool(payload.get("shared", False)),
                    json.dumps(config, sort_keys=True),
                )
        return {"storage": storage_id, "type": storage_type, "config": config}

    async def storage_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        storage_id = str(values(inputs)["storage"])
        row = await _storage_row(request, None, storage_id)
        config = state(row["config"])
        return {
            "storage": storage_id,
            "type": str(row["storage_type"]),
            "shared": int(bool(row["shared"])),
            **config,
        }

    async def storage_update(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        storage_id = str(values(inputs)["storage"])
        row = await database(request).pool.fetchrow(
            """SELECT s.resource_id, s.config FROM storages s WHERE s.storage_id=$1""",
            storage_id,
        )
        if row is None:
            raise ApiError(404, "storage does not exist")
        current = state(row["config"])
        provided = values(inputs)
        updated = {
            **current,
            **{
                key: value
                for key, value in provided.items()
                if key not in {"storage", "delete", "digest"}
            },
        }
        await database(request).pool.execute(
            "UPDATE storages SET config=$2::jsonb WHERE storage_id=$1",
            storage_id,
            json.dumps(updated, sort_keys=True),
        )
        return updated

    async def storage_delete(request: Request, inputs: dict[str, Any]) -> None:
        storage_id = str(values(inputs)["storage"])
        status = await database(request).pool.execute(
            """DELETE FROM resources r USING storages s
            WHERE s.resource_id=r.id AND s.storage_id=$1""",
            storage_id,
        )
        if status != "DELETE 1":
            raise ApiError(404, "storage does not exist")

    async def node_storage_list(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        rows = await database(request).pool.fetch(
            """SELECT s.storage_id, s.storage_type, s.shared,
                      s.capacity_bytes, s.used_bytes, s.config
            FROM storages s
            JOIN resources r ON r.id = s.resource_id
            JOIN nodes n ON n.id = r.node_id
            WHERE n.name=$1 OR s.shared = true
            ORDER BY s.storage_id""",
            node,
        )
        return [storage_payload(row) for row in rows]

    async def node_storage_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        return subdirs("content", "status", "upload")

    async def node_storage_status(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        await require_node(request, node)
        row = await _storage_row(request, None, storage_id)
        return storage_payload(row)

    async def node_storage_content(
        request: Request, inputs: dict[str, Any]
    ) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        contents = await database(request).pool.fetch(
            """SELECT volume_id, content_type, size_bytes, metadata, created_at
            FROM storage_contents WHERE storage_resource_id=$1 ORDER BY created_at DESC""",
            resource_id,
        )
        backups = await database(request).pool.fetch(
            """SELECT b.volume_id, b.size_bytes, b.metadata, b.created_at, r.external_id AS vmid
            FROM backups b
            LEFT JOIN resources r ON r.id = b.resource_id
            WHERE b.storage_resource_id=$1
            ORDER BY b.created_at DESC""",
            resource_id,
        )
        result: list[dict[str, Any]] = []
        for item in contents:
            metadata = state(item["metadata"])
            result.append(
                {
                    "volid": str(item["volume_id"]),
                    "content": str(item["content_type"]),
                    "size": int(item["size_bytes"]),
                    "format": metadata.get("format", "raw"),
                    "ctime": int(item["created_at"].timestamp()),
                }
            )
        for item in backups:
            metadata = state(item["metadata"])
            result.append(
                {
                    "volid": str(item["volume_id"]),
                    "content": "backup",
                    "size": int(item["size_bytes"]),
                    "format": "vma.zst",
                    "vmid": int(item["vmid"]) if item["vmid"] is not None else None,
                    "notes": metadata.get("notes-template"),
                    "ctime": int(item["created_at"].timestamp()),
                }
            )
        return result

    async def _content_item(
        request: Request, storage_resource_id: object, volume_id: str
    ) -> dict[str, Any]:
        row = await database(request).pool.fetchrow(
            """SELECT volume_id, content_type, size_bytes, metadata, created_at
            FROM storage_contents WHERE storage_resource_id=$1 AND volume_id=$2""",
            storage_resource_id,
            volume_id,
        )
        if row is not None:
            metadata = state(row["metadata"])
            return {
                "volid": str(row["volume_id"]),
                "content": str(row["content_type"]),
                "size": int(row["size_bytes"]),
                "format": metadata.get("format", "raw"),
                "ctime": int(row["created_at"].timestamp()),
            }
        backup = await database(request).pool.fetchrow(
            """SELECT b.volume_id, b.size_bytes, b.metadata, b.created_at, r.external_id AS vmid
            FROM backups b
            LEFT JOIN resources r ON r.id = b.resource_id
            WHERE b.storage_resource_id=$1 AND b.volume_id=$2""",
            storage_resource_id,
            volume_id,
        )
        if backup is None:
            raise ApiError(404, "volume does not exist")
        metadata = state(backup["metadata"])
        return {
            "volid": str(backup["volume_id"]),
            "content": "backup",
            "size": int(backup["size_bytes"]),
            "format": "vma.zst",
            "vmid": int(backup["vmid"]) if backup["vmid"] is not None else None,
            "notes": metadata.get("notes-template"),
            "ctime": int(backup["created_at"].timestamp()),
        }

    async def node_storage_content_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        volume_id = str(values(inputs)["volume"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        return await _content_item(request, resource_id, volume_id)

    async def node_storage_content_delete(request: Request, inputs: dict[str, Any]) -> None:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        volume_id = str(values(inputs)["volume"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        status = await database(request).pool.execute(
            "DELETE FROM storage_contents WHERE storage_resource_id=$1 AND volume_id=$2",
            resource_id,
            volume_id,
        )
        if status == "DELETE 1":
            return
        status = await database(request).pool.execute(
            "DELETE FROM backups WHERE storage_resource_id=$1 AND volume_id=$2",
            resource_id,
            volume_id,
        )
        if status != "DELETE 1":
            raise ApiError(404, "volume does not exist")

    async def node_storage_upload(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        payload = values(inputs)
        filename = str(payload.get("filename") or "upload.bin")
        content_type = str(payload.get("content") or "iso")
        raw_size = payload.get("size") or 0
        try:
            size = int(raw_size)
        except (TypeError, ValueError) as error:
            raise ApiError(400, "invalid size") from error
        volume_id = f"{storage_id}:{content_type}/{filename}"
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        await database(request).pool.execute(
            """INSERT INTO storage_contents(
                id, storage_resource_id, volume_id, content_type, size_bytes, metadata
            ) VALUES(gen_random_uuid(), $1, $2, $3, $4, $5::jsonb)
            ON CONFLICT (storage_resource_id, volume_id) DO UPDATE
            SET size_bytes=EXCLUDED.size_bytes,
                content_type=EXCLUDED.content_type,
                metadata=EXCLUDED.metadata""",
            resource_id,
            volume_id,
            content_type,
            size,
            json.dumps({"filename": filename, "source": "upload"}, sort_keys=True),
        )
        return {"uploadid": volume_id, "filename": filename, "size": size, "volid": volume_id}

    async def node_storage_prunebackups(
        request: Request, inputs: dict[str, Any]
    ) -> list[dict[str, Any]] | None:
        node = str(values(inputs)["node"])
        storage_id = str(values(inputs)["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        if request.method == "DELETE":
            keep = int(values(inputs).get("keep-last") or values(inputs).get("keep_last") or 1)
            await database(request).pool.execute(
                """DELETE FROM backups
                WHERE storage_resource_id=$1 AND id IN (
                    SELECT id FROM backups
                    WHERE storage_resource_id=$1
                    ORDER BY created_at DESC
                    OFFSET $2
                )""",
                resource_id,
                keep,
            )
            return None
        rows = await database(request).pool.fetch(
            """SELECT volume_id, size_bytes, created_at FROM backups
            WHERE storage_resource_id=$1 ORDER BY created_at DESC""",
            resource_id,
        )
        return [
            {
                "volid": str(row["volume_id"]),
                "size": int(row["size_bytes"]),
                "ctime": int(row["created_at"].timestamp()),
            }
            for row in rows
        ]

    async def content_copy(request: Request, inputs: dict[str, Any]) -> str:
        payload = values(inputs)
        node = str(payload["node"])
        storage_id = str(payload["storage"])
        volume = str(payload["volume"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        row = await database(request).pool.fetchrow(
            """SELECT volume_id, content_type, size_bytes, metadata
            FROM storage_contents WHERE storage_resource_id=$1 AND volume_id=$2""",
            resource_id,
            volume,
        )
        if row is None:
            raise ApiError(404, "volume does not exist")
        target = str(payload.get("target") or f"{volume}-copy")
        await database(request).pool.execute(
            """INSERT INTO storage_contents(
                id, storage_resource_id, volume_id, content_type, size_bytes, metadata
            ) VALUES(gen_random_uuid(), $1, $2, $3, $4, $5::jsonb)
            ON CONFLICT (storage_resource_id, volume_id) DO UPDATE
            SET size_bytes=EXCLUDED.size_bytes, metadata=EXCLUDED.metadata""",
            resource_id,
            target,
            row["content_type"],
            row["size_bytes"],
            json.dumps({**state(row["metadata"]), "copied_from": volume}, sort_keys=True),
        )
        return f"UPID:{node}:copy:{target}"

    async def content_update(request: Request, inputs: dict[str, Any]) -> None:
        payload = values(inputs)
        node = str(payload["node"])
        storage_id = str(payload["storage"])
        volume = str(payload["volume"])
        await require_node(request, node)
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        row = await database(request).pool.fetchrow(
            """SELECT metadata FROM storage_contents
            WHERE storage_resource_id=$1 AND volume_id=$2""",
            resource_id,
            volume,
        )
        if row is None:
            raise ApiError(404, "volume does not exist")
        meta = state(row["metadata"])
        if "notes" in payload:
            meta["notes"] = payload["notes"]
        if "protected" in payload:
            meta["protected"] = int(bool(payload["protected"]))
        await database(request).pool.execute(
            """UPDATE storage_contents SET metadata=$3::jsonb
            WHERE storage_resource_id=$1 AND volume_id=$2""",
            resource_id,
            volume,
            json.dumps(meta, sort_keys=True),
        )

    async def download_url(request: Request, inputs: dict[str, Any]) -> str:
        payload = values(inputs)
        node = str(payload["node"])
        storage_id = str(payload["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        filename = str(payload.get("filename") or "download.bin")
        content_type = str(payload.get("content") or "iso")
        volume_id = f"{storage_id}:{content_type}/{filename}"
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        await database(request).pool.execute(
            """INSERT INTO storage_contents(
                id, storage_resource_id, volume_id, content_type, size_bytes, metadata
            ) VALUES(gen_random_uuid(), $1, $2, $3, 0, $4::jsonb)
            ON CONFLICT (storage_resource_id, volume_id) DO UPDATE
            SET metadata=EXCLUDED.metadata""",
            resource_id,
            volume_id,
            content_type,
            json.dumps(
                {
                    "filename": filename,
                    "url": payload.get("url"),
                    "source": "download-url",
                },
                sort_keys=True,
            ),
        )
        return f"UPID:{node}:download:{filename}"

    async def oci_pull(request: Request, inputs: dict[str, Any]) -> str:
        payload = values(inputs)
        node = str(payload["node"])
        storage_id = str(payload["storage"])
        await require_node(request, node)
        await _storage_row(request, None, storage_id)
        reference = str(payload.get("reference") or "image:latest")
        filename = str(payload.get("filename") or reference.replace("/", "_"))
        volume_id = f"{storage_id}:import/{filename}"
        resource_id = await database(request).pool.fetchval(
            "SELECT resource_id FROM storages WHERE storage_id=$1",
            storage_id,
        )
        await database(request).pool.execute(
            """INSERT INTO storage_contents(
                id, storage_resource_id, volume_id, content_type, size_bytes, metadata
            ) VALUES(gen_random_uuid(), $1, $2, 'import', 0, $3::jsonb)
            ON CONFLICT (storage_resource_id, volume_id) DO UPDATE
            SET metadata=EXCLUDED.metadata""",
            resource_id,
            volume_id,
            json.dumps({"reference": reference, "source": "oci"}, sort_keys=True),
        )
        return f"UPID:{node}:oci-pull:{filename}"

    async def file_restore_list(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        await _storage_row(request, None, str(payload["storage"]))
        filepath = str(payload.get("filepath") or "/")
        return [{"filepath": filepath.rstrip("/") + "/etc", "type": "d", "text": "etc"}]

    async def file_restore_download(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        await _storage_row(request, None, str(payload["storage"]))
        return {
            "download-url": f"/api2/json/nodes/{payload['node']}/storage/"
            f"{payload['storage']}/file-restore/download",
            "filepath": payload.get("filepath") or "/",
            "volume": payload.get("volume"),
        }

    async def storage_identity(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        row = await _storage_row(request, None, str(payload["storage"]))
        return {
            "storage": str(row["storage_id"]),
            "type": str(row["storage_type"]),
            "fingerprint": f"sim-{row['storage_id']}",
        }

    async def import_metadata(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        storage_id = str(payload["storage"])
        volume = str(payload["volume"])
        await _storage_row(request, None, storage_id)
        return {
            "type": "qemu",
            "source": volume,
            "disks": {"scsi0": f"{storage_id}:0/vm-import.raw"},
            "net0": "virtio,bridge=vmbr0",
        }

    async def storage_rrd(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        storage_id = str(payload["storage"])
        await _storage_row(request, None, storage_id)
        return {"filename": f"pve-storage-{storage_id}.rrd"}

    async def storage_rrddata(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        payload = values(inputs)
        await require_node(request, str(payload["node"]))
        await _storage_row(request, None, str(payload["storage"]))
        return [
            {"time": 1_700_000_000, "used": 10, "total": 100},
            {"time": 1_700_000_060, "used": 12, "total": 100},
        ]

    registry.register("/storage", "GET", storage_ids)
    registry.register("/storage", "POST", storage_create)
    registry.register("/storage/{storage}", "GET", storage_get)
    registry.register("/storage/{storage}", "PUT", storage_update)
    registry.register("/storage/{storage}", "DELETE", storage_delete)
    registry.register("/nodes/{node}/storage", "GET", node_storage_list)
    registry.register("/nodes/{node}/storage/{storage}", "GET", node_storage_index)
    registry.register("/nodes/{node}/storage/{storage}/status", "GET", node_storage_status)
    registry.register("/nodes/{node}/storage/{storage}/content", "GET", node_storage_content)
    registry.register("/nodes/{node}/storage/{storage}/content", "POST", node_storage_upload)
    registry.register(
        "/nodes/{node}/storage/{storage}/content/{volume}", "GET", node_storage_content_get
    )
    registry.register(
        "/nodes/{node}/storage/{storage}/content/{volume}", "DELETE", node_storage_content_delete
    )
    registry.register("/nodes/{node}/storage/{storage}/upload", "POST", node_storage_upload)
    registry.register(
        "/nodes/{node}/storage/{storage}/prunebackups", "GET", node_storage_prunebackups
    )
    registry.register(
        "/nodes/{node}/storage/{storage}/prunebackups", "DELETE", node_storage_prunebackups
    )
    registry.register("/nodes/{node}/storage/{storage}/content/{volume}", "POST", content_copy)
    registry.register("/nodes/{node}/storage/{storage}/content/{volume}", "PUT", content_update)
    registry.register("/nodes/{node}/storage/{storage}/download-url", "POST", download_url)
    registry.register("/nodes/{node}/storage/{storage}/oci-registry-pull", "POST", oci_pull)
    registry.register("/nodes/{node}/storage/{storage}/file-restore/list", "GET", file_restore_list)
    registry.register(
        "/nodes/{node}/storage/{storage}/file-restore/download", "GET", file_restore_download
    )
    registry.register("/nodes/{node}/storage/{storage}/identity", "GET", storage_identity)
    registry.register("/nodes/{node}/storage/{storage}/import-metadata", "GET", import_metadata)
    registry.register("/nodes/{node}/storage/{storage}/rrd", "GET", storage_rrd)
    registry.register("/nodes/{node}/storage/{storage}/rrddata", "GET", storage_rrddata)
