"""VM lifecycle operations used by REST and SOAP."""

from __future__ import annotations

import copy
import json
import secrets
from typing import Any

from app.db.pool import Database
from app.vsphere import inventory
from app.vsphere.domain import tasks as task_store
from app.vsphere.errors import invalid_argument, not_found


async def require_vm(database: Database, vm: str) -> inventory.ManagedObject:
    obj = await inventory.get_object(database, vm)
    if obj is None or obj.type != "VirtualMachine":
        raise not_found(f"VM {vm} not found")
    return obj


def _default_identity(name: str, moid: str) -> dict[str, str]:
    digits = "".join(ch for ch in moid if ch.isdigit()) or "0"
    n = int(digits) % 10_000_000_000_000
    return {
        "name": name,
        "instance_uuid": f"5029aaaa-bbbb-cccc-dddd-{n:012d}",
        "bios_uuid": f"4200aaaa-bbbb-cccc-dddd-{n:012d}",
    }


def _default_disk(capacity: int = 42949672960) -> dict[str, Any]:
    return {
        "key": "2000",
        "value": {"label": "Hard disk 1", "capacity": capacity, "type": "SCSI"},
    }


def _default_nic(network: str = "network-41", *, mac_tail: str = "01") -> dict[str, Any]:
    return {
        "key": "4000",
        "value": {
            "label": "Network adapter 1",
            "mac": f"00:50:56:01:00:{mac_tail}",
            "state": "NOT_CONNECTED",
            "type": "VMXNET3",
            "backing": {"type": "STANDARD_PORTGROUP", "network": network},
        },
    }


async def create_vm(
    database: Database,
    *,
    name: str,
    folder: str = "group-v23",
    host: str = "host-11",
    datastore: str = "datastore-31",
    resource_pool: str = "resgroup-22",
    guest_os: str = "OTHER_GUEST_64",
    cpu_count: int = 1,
    memory_size_mib: int = 1024,
    networks: list[str] | None = None,
    disks: list[dict[str, Any]] | None = None,
    nics: list[dict[str, Any]] | None = None,
    template: bool = False,
    power_on: bool = False,
) -> tuple[str, str]:
    """Create a VirtualMachine MO and a CIS/vim task. Returns (moid, task_id)."""

    name = name.strip()
    if not name:
        raise invalid_argument("name is required")
    nets = networks or ["network-41"]
    moid = await inventory.next_moid(database, "vm")
    mac_tail = f"{int(''.join(ch for ch in moid if ch.isdigit()) or '1') % 256:02x}"
    props: dict[str, Any] = {
        "power_state": "POWERED_ON" if power_on else "POWERED_OFF",
        "cpu_count": cpu_count,
        "memory_size_mib": memory_size_mib,
        "guest_OS": guest_os,
        "hardware_version": "VMX_19",
        "host": host,
        "datastore": datastore,
        "resource_pool": resource_pool,
        "networks": nets,
        "template": template,
        "tools_status": "GUEST_TOOLS_NOT_RUNNING",
        "identity": _default_identity(name, moid),
        "disks": disks if disks is not None else [_default_disk()],
        "nics": nics if nics is not None else [_default_nic(nets[0], mac_tail=mac_tail)],
        "cdroms": [],
        "scsi_adapters": [{"adapter": "1000", "type": "LSILOGIC", "sharing": "NONE"}],
        "boot": {"type": "BIOS", "delay": 0},
        "guest_filesystems": {
            "filesystems": {"/": {"capacity": 42949672960, "free_space": 21474836480}}
        },
        "guest_files": {},
    }
    await inventory.upsert_object(
        database,
        moid=moid,
        type_name="VirtualMachine",
        name=name,
        parent_moid=folder,
        props=props,
    )
    task_id = await task_store.create_task(
        database,
        description=f"CreateVM {moid}",
        service="vim.Folder",
        operation="create_vm",
        result={"vm": moid},
    )
    return moid, task_id


async def set_power(database: Database, vm: str, action: str) -> str:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    current = props.get("power_state", "POWERED_OFF")
    mapping = {
        "start": "POWERED_ON",
        "stop": "POWERED_OFF",
        "suspend": "SUSPENDED",
        "reset": "POWERED_ON",
    }
    if action not in mapping:
        raise invalid_argument(f"unsupported power action: {action}")
    if action == "start" and current == "POWERED_ON":
        raise invalid_argument("VM is already powered on")
    if action == "stop" and current == "POWERED_OFF":
        raise invalid_argument("VM is already powered off")
    props["power_state"] = mapping[action]
    await inventory.update_props(database, vm, props)
    return await task_store.create_task(
        database,
        description=f"Power {action} {vm}",
        service="com.vmware.vcenter.vm.power",
        operation=action,
        result={"vm": vm, "power_state": props["power_state"]},
    )


async def clone_vm(
    database: Database,
    *,
    source_vm: str,
    name: str,
    folder: str | None = None,
    host: str | None = None,
    datastore: str | None = None,
    resource_pool: str | None = None,
    power_on: bool = False,
) -> tuple[str, str]:
    src = await require_vm(database, source_vm)
    moid = await inventory.next_moid(database, "vm")
    props = copy.deepcopy(src.props)
    props["power_state"] = "POWERED_ON" if power_on else "POWERED_OFF"
    props["template"] = False
    props["identity"] = _default_identity(name, moid)
    if host:
        props["host"] = host
    if datastore:
        props["datastore"] = datastore
    if resource_pool:
        props["resource_pool"] = resource_pool
    await inventory.upsert_object(
        database,
        moid=moid,
        type_name="VirtualMachine",
        name=name,
        parent_moid=folder or src.parent_moid or "group-v23",
        props=props,
    )
    task_id = await task_store.create_task(
        database,
        description=f"Clone {source_vm} -> {moid}",
        service="com.vmware.vcenter.vm",
        operation="clone",
        result={"vm": moid},
    )
    return moid, task_id


async def customize_vm(database: Database, vm: str, spec: dict[str, Any]) -> str:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    props["customization"] = spec
    if spec.get("hostname") or spec.get("hostName"):
        identity = dict(props.get("identity") or {})
        identity["name"] = str(spec.get("hostname") or spec.get("hostName"))
        props["identity"] = identity
    if spec.get("ip") or spec.get("ipAddress"):
        props["guest_ip"] = str(spec.get("ip") or spec.get("ipAddress"))
    await inventory.update_props(database, vm, props)
    return await task_store.create_task(
        database,
        description=f"Customize {vm}",
        service="vim.VirtualMachine",
        operation="customize",
        result={"vm": vm},
    )


async def guest_list_files(database: Database, vm: str, path: str = "/") -> list[dict[str, Any]]:
    obj = await require_vm(database, vm)
    files = dict(obj.props.get("guest_files") or {})
    # Always expose synthetic root filesystem listing.
    entries = [
        {"path": "/", "type": "DIRECTORY", "size": 0},
        {"path": "/tmp", "type": "DIRECTORY", "size": 0},
        {"path": "/etc", "type": "DIRECTORY", "size": 0},
    ]
    prefix = path.rstrip("/") or ""
    for file_path, meta in files.items():
        if prefix and not str(file_path).startswith(prefix) and file_path != path:
            continue
        entries.append(
            {
                "path": file_path,
                "type": "FILE",
                "size": int(
                    (meta or {}).get("size") or len(str((meta or {}).get("content") or ""))
                ),
            }
        )
    return entries


async def guest_write_file(
    database: Database,
    vm: str,
    path: str,
    content: str,
    *,
    overwrite: bool = True,
) -> None:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    files = dict(props.get("guest_files") or {})
    if path in files and not overwrite:
        raise invalid_argument(f"File exists: {path}")
    files[path] = {"content": content, "size": len(content.encode("utf-8"))}
    props["guest_files"] = files
    await inventory.update_props(database, vm, props)


async def guest_read_file(database: Database, vm: str, path: str) -> str:
    obj = await require_vm(database, vm)
    files = dict(obj.props.get("guest_files") or {})
    if path not in files:
        # Seed a lab default for common paths.
        if path in {"/etc/hostname", "/etc/hosts"}:
            return (
                obj.name
                if path.endswith("hostname")
                else f"127.0.0.1 localhost\n10.0.0.1 {obj.name}\n"
            )
        raise not_found(f"Guest file not found: {path}")
    return str((files[path] or {}).get("content") or "")


async def guest_delete_file(database: Database, vm: str, path: str) -> None:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    files = dict(props.get("guest_files") or {})
    if path not in files:
        raise not_found(f"Guest file not found: {path}")
    del files[path]
    props["guest_files"] = files
    await inventory.update_props(database, vm, props)


async def relocate_vm(database: Database, vm: str, host: str | None, datastore: str | None) -> str:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    if host:
        props["host"] = host
    if datastore:
        props["datastore"] = datastore
    await inventory.update_props(database, vm, props)
    return await task_store.create_task(
        database,
        description=f"Relocate {vm}",
        service="com.vmware.vcenter.vm",
        operation="relocate",
        result={"vm": vm, "host": props.get("host"), "datastore": props.get("datastore")},
    )


def _pool(database: Database) -> Any:
    return database.pool  # type: ignore[attr-defined]


async def list_snapshots(database: Database, vm: str) -> list[dict[str, Any]]:
    await require_vm(database, vm)
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            "SELECT id, name, description, created_at, props FROM vsphere_snapshots WHERE vm_moid = $1 ORDER BY created_at",
            vm,
        )
    out: list[dict[str, Any]] = []
    for row in rows:
        props = row["props"]
        if isinstance(props, str):
            props = json.loads(props)
        out.append(
            {
                "snapshot": row["id"],
                "name": row["name"],
                "description": row["description"],
                "created_at": row["created_at"].isoformat(),
                **(props or {}),
            }
        )
    return out


async def create_snapshot(
    database: Database,
    vm: str,
    *,
    name: str,
    description: str = "",
    memory: bool = False,
) -> tuple[str, str]:
    await require_vm(database, vm)
    snap_id = f"snapshot-{secrets.token_hex(4)}"
    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO vsphere_snapshots (id, vm_moid, name, description, props)
            VALUES ($1, $2, $3, $4, $5::jsonb)
            """,
            snap_id,
            vm,
            name,
            description,
            json.dumps({"memory": memory}),
        )
    task_id = await task_store.create_task(
        database,
        description=f"Create snapshot {snap_id}",
        service="com.vmware.vcenter.vm.snapshots",
        operation="create",
        result={"snapshot": snap_id},
    )
    return snap_id, task_id


async def delete_snapshot(database: Database, vm: str, snapshot: str) -> str:
    await require_vm(database, vm)
    pool = _pool(database)
    async with pool.acquire() as conn:
        result = await conn.execute(
            "DELETE FROM vsphere_snapshots WHERE id = $1 AND vm_moid = $2",
            snapshot,
            vm,
        )
    if not result.endswith("1"):
        raise not_found(f"Snapshot {snapshot} not found")
    return await task_store.create_task(
        database,
        description=f"Delete snapshot {snapshot}",
        service="com.vmware.vcenter.vm.snapshots",
        operation="delete",
        result={"snapshot": snapshot},
    )


async def revert_snapshot(database: Database, vm: str, snapshot: str) -> str:
    await require_vm(database, vm)
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT id FROM vsphere_snapshots WHERE id = $1 AND vm_moid = $2",
            snapshot,
            vm,
        )
    if row is None:
        raise not_found(f"Snapshot {snapshot} not found")
    return await task_store.create_task(
        database,
        description=f"Revert snapshot {snapshot}",
        service="com.vmware.vcenter.vm.snapshots",
        operation="revert",
        result={"snapshot": snapshot, "vm": vm},
    )


async def update_hardware_cpu(database: Database, vm: str, count: int) -> None:
    obj = await require_vm(database, vm)
    cpu = dict(obj.props.get("cpu") or {})
    if obj.props.get("power_state") == "POWERED_ON" and not cpu.get("hot_add_enabled"):
        raise invalid_argument(
            "Virtual machine must be powered off to reconfigure CPU count "
            "when CPU hot-add is disabled"
        )
    props = dict(obj.props)
    props["cpu_count"] = count
    cpu["count"] = count
    props["cpu"] = cpu
    await inventory.update_props(database, vm, props)


async def update_hardware_memory(database: Database, vm: str, size_mib: int) -> None:
    obj = await require_vm(database, vm)
    memory = dict(obj.props.get("memory") or {})
    if obj.props.get("power_state") == "POWERED_ON" and not memory.get("hot_add_enabled"):
        raise invalid_argument(
            "Virtual machine must be powered off to reconfigure memory "
            "when memory hot-add is disabled"
        )
    props = dict(obj.props)
    props["memory_size_mib"] = size_mib
    memory["size_MiB"] = size_mib
    props["memory"] = memory
    await inventory.update_props(database, vm, props)


async def add_disk(database: Database, vm: str, capacity: int) -> dict[str, Any]:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    disks = list(props.get("disks") or [])
    key = str(2000 + len(disks))
    disk = {
        "key": key,
        "value": {"label": f"Hard disk {len(disks) + 1}", "capacity": capacity, "type": "SCSI"},
    }
    disks.append(disk)
    props["disks"] = disks
    await inventory.update_props(database, vm, props)
    return disk


async def add_nic(database: Database, vm: str, network: str = "network-41") -> dict[str, Any]:
    obj = await require_vm(database, vm)
    props = dict(obj.props)
    nics = list(props.get("nics") or [])
    key = str(4000 + len(nics))
    nic = {
        "key": key,
        "value": {
            "label": f"Network adapter {len(nics) + 1}",
            "mac": f"00:50:56:01:{len(nics):02x}:{obj.moid[-2:]}",
            "state": "CONNECTED",
            "backing": {"type": "STANDARD_PORTGROUP", "network": network},
        },
    }
    nics.append(nic)
    props["nics"] = nics
    networks = list(props.get("networks") or [])
    if network not in networks:
        networks.append(network)
    props["networks"] = networks
    await inventory.update_props(database, vm, props)
    return nic


async def set_template(database: Database, vm: str, *, template: bool) -> str:
    obj = await require_vm(database, vm)
    if template and obj.props.get("power_state") == "POWERED_ON":
        raise invalid_argument("Power off VM before converting to template")
    props = dict(obj.props)
    props["template"] = template
    await inventory.update_props(database, vm, props)
    return await task_store.create_task(
        database,
        description=f"{'MarkAsTemplate' if template else 'MarkAsVirtualMachine'} {vm}",
        service="com.vmware.vcenter.vm",
        operation="mark_as_template" if template else "mark_as_vm",
        result={"vm": vm, "template": template},
    )


async def unregister_vm(database: Database, vm: str) -> str:
    await require_vm(database, vm)
    await inventory.delete_object(database, vm)
    return await task_store.create_task(
        database,
        description=f"Unregister {vm}",
        service="com.vmware.vcenter.vm",
        operation="unregister",
        result={"vm": vm},
    )


async def guest_networking(database: Database, vm: str) -> dict[str, Any]:
    obj = await require_vm(database, vm)
    networking = obj.props.get("guest_networking")
    return networking if isinstance(networking, dict) else {}


async def console_ticket(database: Database, vm: str) -> dict[str, Any]:
    obj = await require_vm(database, vm)
    import json
    import secrets

    ticket = secrets.token_urlsafe(24)
    payload = {
        "ticket": ticket,
        "cfg_file": f"[{obj.props.get('datastore')}] {obj.name}/{obj.name}.vmx",
        "host": obj.props.get("host"),
        "port": 443,
        "ssl_thumbprint": obj.props.get("ssl_thumbprint") or "",
        "vm": vm,
    }
    pool = database.pool  # type: ignore[attr-defined]
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO vsphere_console_tickets (ticket, vm_moid, payload)
            VALUES ($1, $2, $3::jsonb)
            """,
            ticket,
            vm,
            json.dumps(payload),
        )
    return payload
