"""Declarative vSphere inventory profiles (small lab / large cluster)."""

from __future__ import annotations

import json
import os
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True, slots=True)
class ObjectSpec:
    moid: str
    type: str
    name: str
    parent_moid: str | None
    props: dict[str, Any]


@dataclass(frozen=True, slots=True)
class CredentialSpec:
    username: str
    password: str
    roles: tuple[str, ...]


@dataclass(frozen=True, slots=True)
class PermissionSpec:
    principal: str
    role: str
    entity_moid: str | None
    propagate: bool = True


@dataclass(frozen=True, slots=True)
class VsphereSeedProfile:
    name: str
    objects: tuple[ObjectSpec, ...]
    credentials: tuple[CredentialSpec, ...]
    permissions: tuple[PermissionSpec, ...]
    host_count: int
    vm_count: int
    extras_scale: int = 1


POWER_CYCLE = ("POWERED_ON", "POWERED_ON", "POWERED_ON", "POWERED_OFF", "SUSPENDED")
GUEST_OS = ("UBUNTU_64", "CENTOS_7_64", "WINDOWS_9_64", "RHEL_8_64", "OTHER_GUEST_64")
ROLE_PREFIX = (
    "web",
    "app",
    "db",
    "cache",
    "batch",
    "jump",
    "ci",
    "mon",
    "log",
    "ml",
)


def _topology(
    *,
    host_count: int,
    datastore_count: int = 4,
    network_count: int = 3,
    extra_vm_folders: int = 0,
) -> list[ObjectSpec]:
    specs: list[ObjectSpec] = [
        ObjectSpec("group-d1", "Folder", "Datacenters", None, {"folder_type": "DATACENTER"}),
        ObjectSpec(
            "datacenter-21",
            "Datacenter",
            "Datacenter",
            "group-d1",
            {
                "datastore_folder": "group-s23",
                "host_folder": "group-h23",
                "vm_folder": "group-v23",
                "network_folder": "group-n23",
            },
        ),
        ObjectSpec("group-h23", "Folder", "host", "datacenter-21", {"folder_type": "HOST"}),
        ObjectSpec(
            "group-v23", "Folder", "vm", "datacenter-21", {"folder_type": "VIRTUAL_MACHINE"}
        ),
        ObjectSpec(
            "group-s23", "Folder", "datastore", "datacenter-21", {"folder_type": "DATASTORE"}
        ),
        ObjectSpec("group-n23", "Folder", "network", "datacenter-21", {"folder_type": "NETWORK"}),
        ObjectSpec(
            "domain-c21",
            "ClusterComputeResource",
            "Cluster",
            "group-h23",
            {"drs_enabled": True, "ha_enabled": True, "resource_pool": "resgroup-22"},
        ),
        ObjectSpec(
            "resgroup-22",
            "ResourcePool",
            "Resources",
            "domain-c21",
            {"cpu_limit_mhz": -1, "memory_limit_mib": -1},
        ),
        # Workload folders for realism
        ObjectSpec(
            "group-v100", "Folder", "production", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
        ),
        ObjectSpec(
            "group-v101", "Folder", "staging", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
        ),
        ObjectSpec(
            "group-v102", "Folder", "templates", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
        ),
    ]
    for index in range(extra_vm_folders):
        specs.append(
            ObjectSpec(
                f"group-v{103 + index}",
                "Folder",
                f"team-{index + 1:02d}",
                "group-v23",
                {"folder_type": "VIRTUAL_MACHINE"},
            )
        )
    for index in range(1, host_count + 1):
        moid = f"host-{10 + index}"
        specs.append(
            ObjectSpec(
                moid,
                "HostSystem",
                f"esxi{index:02d}.lab.local",
                "domain-c21",
                {
                    "connection_state": "CONNECTED",
                    "power_state": "POWERED_ON",
                    "cpu_cores": 32 if index % 3 else 64,
                    "cpu_mhz": 2500,
                    "memory_size_mib": 262144 if index % 2 else 524288,
                    "ip_address": f"192.168.1.{10 + index}",
                    "version": "8.0.2",
                    "cluster": "domain-c21",
                    "maintenance_mode": False,
                    "networking": {
                        "dns": {"servers": ["8.8.8.8", "1.1.1.1"], "domains": ["lab.local"]},
                        "routing": {"default_gateway": "192.168.1.1"},
                        "interfaces": [
                            {
                                "name": "vmk0",
                                "mac": f"00:50:56:00:{index:02x}:01",
                                "ipv4": {"address": f"192.168.1.{10 + index}", "prefix": 24},
                            }
                        ],
                    },
                    "storage_devices": [
                        {
                            "device": f"naa.lab{index:04d}",
                            "display_name": f"Local Disk {index}",
                            "capacity": 1099511627776 * (1 + index % 3),
                            "ssd": index % 2 == 0,
                        }
                    ],
                },
            )
        )
    for index in range(1, datastore_count + 1):
        moid = f"datastore-{30 + index}"
        capacity = 1099511627776 * (1 + (index % 3))
        specs.append(
            ObjectSpec(
                moid,
                "Datastore",
                f"ds-{index:02d}" if index > 1 else "datastore1",
                "group-s23",
                {
                    "type": "VMFS" if index % 2 else "NFS",
                    "capacity": capacity,
                    "free_space": capacity // 2,
                    "accessible": True,
                    "multiple_host_access": True,
                },
            )
        )
    specs.append(
        ObjectSpec(
            "network-41",
            "Network",
            "VM Network",
            "group-n23",
            {"type": "STANDARD_PORTGROUP"},
        )
    )
    for index in range(2, network_count + 1):
        specs.append(
            ObjectSpec(
                f"dvportgroup-{40 + index}",
                "DistributedVirtualPortgroup",
                f"dvpg-vlan{100 + index}",
                "group-n23",
                {"type": "DISTRIBUTED_PORTGROUP", "vlan_id": 100 + index},
            )
        )
    specs.append(
        ObjectSpec(
            "dvs-51",
            "VmwareDistributedVirtualSwitch",
            "DSwitch",
            "group-n23",
            {"version": "8.0.0", "mtu": 9000},
        )
    )
    return specs


def _vm_device_props(*, name: str, power: str, index: int, nic_mac: str) -> dict[str, Any]:
    """Shared hardware / guest fields for lab VMs (used by API surface + SOAP)."""

    guest_ip = f"10.0.{(index // 250) % 250}.{(index % 250) or 1}"
    return {
        "nics": [
            {
                "key": "4000",
                "value": {
                    "label": "Network adapter 1",
                    "mac": nic_mac,
                    "state": "CONNECTED" if power == "POWERED_ON" else "NOT_CONNECTED",
                    "type": "VMXNET3",
                    "backing": {"type": "STANDARD_PORTGROUP", "network": "network-41"},
                },
            }
        ],
        "disks": [
            {
                "key": "2000",
                "value": {
                    "label": "Hard disk 1",
                    "capacity": 42949672960 + (index % 5) * 10737418240,
                    "type": "SCSI",
                },
            }
        ],
        "cdroms": [
            {
                "cdrom": "3000",
                "label": "CD/DVD drive 1",
                "state": "CONNECTED",
                "backing": {"type": "ISO_FILE", "iso_file": "[datastore1] ISO/ubuntu.iso"},
            }
        ],
        "floppies": [{"floppy": "8000", "state": "NOT_CONNECTED"}],
        "serials": [{"port": "9000", "yield_on_poll": True}],
        "parallels": [{"port": "10000", "yield_on_poll": True}],
        "scsi_adapters": [
            {"adapter": "1000", "type": "LSILOGIC", "sharing": "NONE", "pci_slot_number": 16}
        ],
        "sata_adapters": [{"adapter": "15000", "bus": 0, "pci_slot_number": 33}],
        "nvme_adapters": [{"adapter": "19000", "bus": 0, "pci_slot_number": 160}],
        "boot": {
            "type": "BIOS",
            "delay": 0,
            "retry": False,
            "retry_delay": 10000,
            "enter_setup_mode": False,
        },
        "boot_devices": [{"type": "CDROM"}, {"type": "DISK"}, {"type": "ETHERNET"}],
        "guest_ip": guest_ip,
        "guest_filesystems": {
            "filesystems": {
                "/": {"capacity": 42949672960, "free_space": 21474836480},
            }
        },
        "guest_networking": {
            "dns": {"ip_addresses": ["8.8.8.8"], "host_name": name, "domain_name": "lab.local"},
            "ip": {
                "ip_addresses": [
                    {
                        "ip_address": guest_ip,
                        "prefix_length": 24,
                        "state": "PREFERRED",
                    }
                ]
            },
        },
        "customization": {
            "name": name,
            "status": "PENDING",
            "spec": {"hostname": name, "domain": "lab.local"},
        },
        "tools": {
            "auto_update_supported": True,
            "install_attempted": True,
            "run_state": "RUNNING" if power == "POWERED_ON" else "NOT_RUNNING",
            "upgrade_policy": "MANUAL",
            "version_number": 12320,
            "version_status": "CURRENT",
        },
        "cpu": {"cores_per_socket": 1, "hot_add_enabled": False},
        "memory": {"hot_add_enabled": False},
        "identity": {
            "name": name,
            "instance_uuid": f"5029{index:04d}-0000-0000-0000-{index:012d}",
        },
    }


def _vm_spec(
    index: int,
    *,
    host_count: int,
    datastore_count: int = 4,
    folder_choices: tuple[str, ...] | None = None,
) -> ObjectSpec:
    moid = f"vm-{100 + index}"
    role = ROLE_PREFIX[index % len(ROLE_PREFIX)]
    name = f"{role}-{index:04d}"
    power = POWER_CYCLE[index % len(POWER_CYCLE)]
    host = f"host-{10 + (index % host_count) + 1}"
    cpus = 1 + (index % 8)
    memory = 1024 * (1 + (index % 16))
    folders = folder_choices or ("group-v100", "group-v101", "group-v23")
    folder = folders[index % len(folders)]
    guest = GUEST_OS[index % len(GUEST_OS)]
    ds_index = 1 + (index % max(1, datastore_count))
    nic_tail = f"{(index % 250):02x}"
    devices = _vm_device_props(
        name=name,
        power=power,
        index=index,
        nic_mac=f"00:50:56:01:{(index // 256) % 256:02x}:{nic_tail}",
    )
    return ObjectSpec(
        moid,
        "VirtualMachine",
        name,
        folder,
        {
            "power_state": power,
            "cpu_count": cpus,
            "memory_size_mib": memory,
            "guest_OS": guest,
            "hardware_version": "VMX_19",
            "host": host,
            "datastore": f"datastore-{30 + ds_index}",
            "resource_pool": "resgroup-22",
            "networks": ["network-41"],
            "template": False,
            "tools_status": "GUEST_TOOLS_RUNNING"
            if power == "POWERED_ON"
            else "GUEST_TOOLS_NOT_RUNNING",
            **devices,
        },
    )


def lab_credentials() -> tuple[CredentialSpec, ...]:
    return (
        CredentialSpec("administrator@vsphere.local", "VMware1!", ("Administrator",)),
        CredentialSpec("readonly@vsphere.local", "VMware1!", ("ReadOnly",)),
        CredentialSpec("operator@vsphere.local", "VMware1!", ("VirtualMachinePowerUser",)),
        CredentialSpec("vmadmin@vsphere.local", "VMware1!", ("VirtualMachineAdministrator",)),
    )


def lab_permissions() -> tuple[PermissionSpec, ...]:
    return (
        PermissionSpec("administrator@vsphere.local", "Administrator", None, True),
        PermissionSpec("readonly@vsphere.local", "ReadOnly", "datacenter-21", True),
        PermissionSpec("operator@vsphere.local", "VirtualMachinePowerUser", "group-v23", True),
        PermissionSpec("vmadmin@vsphere.local", "VirtualMachineAdministrator", "group-v23", True),
    )


@dataclass(frozen=True, slots=True)
class ProfileSize:
    """Canonical lab sizes: hosts / VMs / datastores / networks / platform extras scale."""

    name: str
    host_count: int
    vm_count: int
    datastore_count: int
    network_count: int
    extras_scale: int


# Proportional inventory tiers shown in the Web UI DATA panel.
PROFILE_SIZES: dict[str, ProfileSize] = {
    # Reset / unload target — cookbook-only inventory.
    "minimal": ProfileSize(
        "minimal", host_count=3, vm_count=5, datastore_count=1, network_count=1, extras_scale=1
    ),
    "small": ProfileSize("small", host_count=3, vm_count=50, datastore_count=2, network_count=2, extras_scale=1),
    "large": ProfileSize(
        "large", host_count=10, vm_count=1000, datastore_count=4, network_count=4, extras_scale=2
    ),
    "big": ProfileSize("big", host_count=20, vm_count=2000, datastore_count=8, network_count=8, extras_scale=4),
}


def _named_lab_vms() -> list[ObjectSpec]:
    """Stable cookbook VMs (vm-101..vm-105) present in every profile."""

    named = (
        ("web-01", "POWERED_ON", "host-11", 2, 4096),
        ("web-02", "POWERED_ON", "host-12", 2, 4096),
        ("db-01", "POWERED_ON", "host-13", 4, 8192),
        ("app-01", "POWERED_OFF", "host-11", 2, 2048),
        ("jumpbox", "SUSPENDED", "host-12", 1, 1024),
    )
    vms: list[ObjectSpec] = []
    for index, (name, power, host, cpus, memory) in enumerate(named, start=1):
        moid = f"vm-{100 + index}"
        devices = _vm_device_props(
            name=name,
            power=power,
            index=index,
            nic_mac=f"00:50:56:01:00:{moid[-2:]}",
        )
        devices["identity"]["instance_uuid"] = f"5029{moid[-3:]}-0000-0000-0000-000000000000"
        vms.append(
            ObjectSpec(
                moid,
                "VirtualMachine",
                name,
                "group-v23",
                {
                    "power_state": power,
                    "cpu_count": cpus,
                    "memory_size_mib": memory,
                    "guest_OS": "UBUNTU_64",
                    "hardware_version": "VMX_19",
                    "host": host,
                    "datastore": "datastore-31",
                    "resource_pool": "resgroup-22",
                    "networks": ["network-41"],
                    "template": False,
                    "tools_status": "GUEST_TOOLS_RUNNING"
                    if power == "POWERED_ON"
                    else "GUEST_TOOLS_NOT_RUNNING",
                    **devices,
                },
            )
        )
    return vms


def _build_sized_profile(size: ProfileSize) -> VsphereSeedProfile:
    if size.host_count < 1 or size.vm_count < 1:
        raise ValueError("host_count and vm_count must be positive")
    # Folders scale with extras: minimal/small=0, large=2, big=6.
    extra_vm_folders = max(0, (size.extras_scale - 1) * 2)
    objects = _topology(
        host_count=size.host_count,
        datastore_count=size.datastore_count,
        network_count=size.network_count,
        extra_vm_folders=extra_vm_folders,
    )
    named = _named_lab_vms()
    # Minimal / single-datastore profiles still reference datastore-31.
    if size.datastore_count < 1:
        raise ValueError("datastore_count must be positive")
    folder_choices = (
        "group-v100",
        "group-v101",
        "group-v23",
        *(f"group-v{103 + i}" for i in range(extra_vm_folders)),
    )
    vms: list[ObjectSpec] = list(named)
    if size.vm_count > len(named):
        vms.extend(
            _vm_spec(
                index,
                host_count=size.host_count,
                datastore_count=size.datastore_count,
                folder_choices=folder_choices,
            )
            for index in range(len(named) + 1, size.vm_count + 1)
        )
    elif size.vm_count < len(named):
        vms = vms[: size.vm_count]
    return VsphereSeedProfile(
        name=size.name,
        objects=tuple(objects + vms),
        credentials=lab_credentials(),
        permissions=lab_permissions(),
        host_count=size.host_count,
        vm_count=len(vms),
        extras_scale=size.extras_scale,
    )


def minimal_vsphere_profile() -> VsphereSeedProfile:
    """Reset target: 3 hosts · 5 cookbook VMs · 1 datastore · 1 network."""

    return _build_sized_profile(PROFILE_SIZES["minimal"])


def small_vsphere_profile() -> VsphereSeedProfile:
    """Lab tier: 3 hosts · 50 VMs · 2 datastores · 2 networks."""

    return _build_sized_profile(PROFILE_SIZES["small"])


def large_vsphere_profile(*, host_count: int = 10, vm_count: int = 1000) -> VsphereSeedProfile:
    """Lab tier: 10 hosts · 1000 VMs (defaults); kwargs keep Makefile overrides."""

    size = PROFILE_SIZES["large"]
    if host_count == size.host_count and vm_count == size.vm_count:
        return _build_sized_profile(size)
    # Custom scale: keep datastore/network proportion to hosts (≈0.4× hosts, min 2).
    datastore_count = max(2, round(host_count * 0.4))
    network_count = max(2, round(host_count * 0.4))
    return _build_sized_profile(
        ProfileSize(
            name="large",
            host_count=host_count,
            vm_count=vm_count,
            datastore_count=datastore_count,
            network_count=network_count,
            extras_scale=max(1, datastore_count // 2),
        )
    )


def big_vsphere_profile() -> VsphereSeedProfile:
    """Lab tier: 20 hosts · 2000 VMs · 8 datastores · 8 networks."""

    return _build_sized_profile(PROFILE_SIZES["big"])


def demo_cluster_vsphere_profile() -> VsphereSeedProfile:
    """Backward-compatible alias for ``big`` (UI / older docs used demo-cluster)."""

    return big_vsphere_profile()


def build_vsphere_profile(
    name: str | None = None,
    *,
    large_hosts: int | None = None,
    large_vms: int | None = None,
) -> VsphereSeedProfile:
    profile_name = (name or os.getenv("SEED_VSPHERE_PROFILE") or "large").strip().lower()
    hosts = (
        large_hosts if large_hosts is not None else int(os.getenv("SEED_VSPHERE_LARGE_HOSTS", "10"))
    )
    vms = large_vms if large_vms is not None else int(os.getenv("SEED_VSPHERE_LARGE_VMS", "1000"))
    if profile_name in {"minimal", "mini", "reset"}:
        return minimal_vsphere_profile()
    if profile_name in {"small"}:
        return small_vsphere_profile()
    if profile_name in {"big", "demo-cluster", "demo", "enterprise"}:
        return big_vsphere_profile()
    if profile_name == "large":
        return large_vsphere_profile(host_count=hosts, vm_count=vms)
    raise ValueError(f"unknown vSphere seed profile: {profile_name}")


def infer_profile_hint(*, hosts: int, vms: int, datastores: int = 0) -> str:
    """Map live inventory counts back to a DATA-panel profile name."""

    for size in PROFILE_SIZES.values():
        if hosts == size.host_count and vms == size.vm_count:
            if datastores and datastores != size.datastore_count:
                continue
            return size.name
    if hosts >= 15 and vms >= 1500:
        return "big"
    if hosts >= 8 and vms >= 500:
        return "large"
    if hosts <= 4 and vms <= 10:
        return "minimal"
    if hosts <= 4:
        return "small"
    return "custom"


def props_json(props: dict[str, Any]) -> str:
    return json.dumps(props)
