from __future__ import annotations

import fnmatch
import re
from dataclasses import replace

from .errors import SettingsError
from .models.workflow import DiskConfig, ReferenceData, VmConfig
from .settings import AppSettings

_NIXOS_ISO_PATTERN = re.compile(
    r"nixos-minimal-(?P<year>\d{2})[.-](?P<month>\d{2})\.[A-Za-z0-9]+-[A-Za-z0-9_]+-linux\.iso$"
)
_REGEX_SELECTOR_PREFIX = "regex:"


def select_latest_nixos_iso(isos: list[str]) -> str | None:
    candidates: list[tuple[int, int, str]] = []
    for iso in isos:
        match = _NIXOS_ISO_PATTERN.search(iso)
        if match:
            candidates.append((int(match.group("year")), int(match.group("month")), iso))
    if not candidates:
        return None
    return max(candidates)[2]


def _matches_iso_selector(iso: str, selector: str) -> bool:
    if selector.startswith(_REGEX_SELECTOR_PREFIX):
        pattern = selector.removeprefix(_REGEX_SELECTOR_PREFIX)
        return re.search(pattern, iso) is not None
    return fnmatch.fnmatch(iso, selector)


def select_preferred_iso(isos: list[str], selector: str | None = None) -> str | None:
    if selector:
        matches = sorted(iso for iso in isos if _matches_iso_selector(iso, selector))
        if matches:
            return select_latest_nixos_iso(matches) or matches[0]
    return select_latest_nixos_iso(isos)


def build_startup_value(order: str, up: str, down: str) -> str:
    parts: list[str] = []
    if order.strip():
        parts.append(f"order={order.strip()}")
    if up.strip():
        parts.append(f"up={up.strip()}")
    if down.strip():
        parts.append(f"down={down.strip()}")
    return ",".join(parts)


def effective_vm_config(config: VmConfig, settings: AppSettings) -> VmConfig:
    result = replace(config)
    result.general = replace(config.general)
    result.general.name = settings.safety_policy.effective_vm_name(config.general.name.strip())

    if not settings.safety_policy.enable_test_mode:
        result.general.tags = [tag for tag in config.general.tags if tag]
        return result

    tags = [tag for tag in config.general.tags if tag]
    if settings.safety_policy.test_tag not in tags:
        tags.append(settings.safety_policy.test_tag)
    result.general.tags = sorted(dict.fromkeys(tags))
    return result


def validate_step(
    step: str,
    config: VmConfig,
    settings: AppSettings,
    references: ReferenceData,
) -> list[str]:
    errors: list[str] = []

    if step == "general":
        if not config.general.node:
            errors.append("Node is required.")
        if config.general.vmid < 100:
            errors.append("VM ID must be at least 100.")
        if not config.general.name.strip():
            errors.append("Name is required.")
        for label, value in [
            ("Startup order", config.general.startup_order),
            ("Startup delay", config.general.startup_delay),
            ("Shutdown timeout", config.general.shutdown_timeout),
        ]:
            if value.strip() and not value.strip().isdigit():
                errors.append(f"{label} must be an integer.")
        if settings.safety_policy.enable_test_mode and settings.safety_policy.test_node:
            if config.general.node != settings.safety_policy.test_node:
                errors.append(
                    f"Live create mode is restricted to node {settings.safety_policy.test_node}."
                )
        if settings.safety_policy.enable_test_mode and settings.safety_policy.test_pool:
            if config.general.pool != settings.safety_policy.test_pool:
                errors.append(
                    f"Live create mode is restricted to pool {settings.safety_policy.test_pool}."
                )

    if step == "os":
        if config.os.media_choice == "iso":
            if not config.os.storage:
                errors.append("ISO storage is required.")
            if not config.os.iso:
                errors.append("ISO selection is required.")
        if config.os.media_choice == "physical" and not config.os.physical_drive_path.strip():
            errors.append("Physical disc drive path is required.")

    if step == "system":
        if config.system.add_efi_disk and not config.system.efi_storage:
            errors.append("EFI storage is required when EFI disk is enabled.")

    if step == "disks":
        slots: set[str] = set()
        for disk in config.disks:
            if disk.slot_name in slots:
                errors.append(f"Duplicate disk slot {disk.slot_name}.")
            slots.add(disk.slot_name)
            if disk.size_gib <= 0:
                errors.append(f"Disk {disk.slot_name} size must be greater than zero.")
            if not disk.storage:
                errors.append(f"Disk {disk.slot_name} storage is required.")

    if step == "cpu":
        if config.cpu.cores <= 0:
            errors.append("CPU cores must be greater than zero.")
        if config.cpu.sockets <= 0:
            errors.append("CPU sockets must be greater than zero.")

    if step == "memory":
        if config.memory.memory_mib <= 0:
            errors.append("Memory must be greater than zero.")
        if config.memory.ballooning:
            if config.memory.min_memory_mib <= 0:
                errors.append("Min memory must be greater than zero when ballooning is enabled.")
            if config.memory.min_memory_mib > config.memory.memory_mib:
                errors.append("Min memory cannot exceed memory size.")

    if step == "network" and not config.network.no_network_device:
        if not config.network.bridge:
            errors.append("Bridge is required unless networking is disabled.")
        for label, value in [
            ("VLAN tag", config.network.vlan_tag),
            ("MTU", config.network.mtu),
            ("Multiqueue", config.network.multiqueue),
        ]:
            if value.strip() and not value.strip().isdigit():
                errors.append(f"{label} must be an integer.")

    if step == "confirm" and not settings.safety_policy.allow_create:
        errors.append("Set PROXMOX_PREVENT_CREATE=false to enable VM creation.")

    return errors


def validate_all_steps(
    config: VmConfig, settings: AppSettings, references: ReferenceData
) -> list[str]:
    all_errors: list[str] = []
    for step in ["general", "os", "system", "disks", "cpu", "memory", "network", "confirm"]:
        all_errors.extend(validate_step(step, config, settings, references))
    return all_errors


def _bool_int(value: bool) -> int:
    return 1 if value else 0


def build_disk_value(disk: DiskConfig) -> str:
    options = [
        f"{disk.storage}:{disk.size_gib}",
        f"format={disk.format}",
        f"cache={disk.cache}",
        f"discard={'on' if disk.discard else 'ignore'}",
        f"iothread={_bool_int(disk.io_thread)}",
        f"ssd={_bool_int(disk.ssd_emulation)}",
        f"backup={_bool_int(disk.backup)}",
        f"replicate={_bool_int(not disk.skip_replication)}",
        f"aio={disk.async_io}",
    ]
    return ",".join(options)


def build_network_value(config: VmConfig) -> str | None:
    if config.network.no_network_device:
        return None

    parts: list[str] = []
    if config.network.mac_address.strip():
        parts.append(f"{config.network.model}={config.network.mac_address.strip()}")
    else:
        parts.append(f"model={config.network.model}")
    parts.append(f"bridge={config.network.bridge}")
    parts.append(f"firewall={_bool_int(config.network.firewall)}")
    parts.append(f"link_down={_bool_int(config.network.disconnected)}")
    if config.network.vlan_tag.strip():
        parts.append(f"tag={int(config.network.vlan_tag)}")
    if config.network.mtu.strip():
        parts.append(f"mtu={int(config.network.mtu)}")
    if config.network.rate_limit.strip():
        parts.append(f"rate={config.network.rate_limit.strip()}")
    if config.network.multiqueue.strip():
        parts.append(f"queues={int(config.network.multiqueue)}")
    return ",".join(parts)


def build_media_value(config: VmConfig) -> str | None:
    if config.os.media_choice == "none":
        return None
    if config.os.media_choice == "iso" and config.os.iso:
        return f"{config.os.iso},media=cdrom"
    if config.os.media_choice == "physical":
        return f"{config.os.physical_drive_path.strip()},media=cdrom"
    return None


def build_create_payload(config: VmConfig, settings: AppSettings) -> dict[str, str | int]:
    if not settings.safety_policy.allow_create:
        raise SettingsError("PROXMOX_PREVENT_CREATE=false is required before creating VMs.")

    effective = effective_vm_config(config, settings)
    payload: dict[str, str | int] = {
        "vmid": effective.general.vmid,
        "name": effective.general.name,
        "ostype": effective.os.guest_version,
        "bios": effective.system.bios,
        "machine": effective.system.machine,
        "scsihw": effective.system.scsi_controller,
        "agent": _bool_int(effective.system.qemu_agent),
        "cores": effective.cpu.cores,
        "sockets": effective.cpu.sockets,
        "cpu": effective.cpu.cpu_type,
        "memory": effective.memory.memory_mib,
        "balloon": effective.memory.min_memory_mib if effective.memory.ballooning else 0,
        "allow-ksm": _bool_int(effective.memory.allow_ksm),
        "onboot": _bool_int(effective.general.onboot),
        "tags": ";".join(effective.general.tags),
    }
    if effective.general.pool:
        payload["pool"] = effective.general.pool
    startup = build_startup_value(
        effective.general.startup_order,
        effective.general.startup_delay,
        effective.general.shutdown_timeout,
    )
    if startup:
        payload["startup"] = startup
    if effective.system.graphic_card != "default":
        payload["vga"] = effective.system.graphic_card
    if effective.system.add_efi_disk:
        payload["efidisk0"] = (
            f"{effective.system.efi_storage}:1,efitype=4m,"
            f"pre-enrolled-keys={_bool_int(effective.system.pre_enrolled_keys)}"
        )
    if effective.system.tpm_enabled:
        payload["tpmstate0"] = f"{effective.system.efi_storage}:4,version=v2.0"
    media = build_media_value(effective)
    if media:
        payload["ide2"] = media
    network = build_network_value(effective)
    if network:
        payload["net0"] = network
    for disk in effective.disks:
        payload[disk.slot_name] = build_disk_value(disk)
    return payload


def build_confirmation_text(config: VmConfig, settings: AppSettings) -> str:
    effective = (
        effective_vm_config(config, settings) if settings.safety_policy.allow_create else config
    )
    startup = build_startup_value(
        effective.general.startup_order,
        effective.general.startup_delay,
        effective.general.shutdown_timeout,
    )
    system_line = (
        f"System: machine={effective.system.machine}, "
        f"bios={effective.system.bios}, scsi={effective.system.scsi_controller}"
    )
    efi_line = (
        "EFI disk: "
        f"{'enabled' if effective.system.add_efi_disk else 'disabled'} "
        f"({effective.system.efi_storage or '-'})"
    )
    cpu_line = (
        f"CPU: {effective.cpu.sockets} socket(s), "
        f"{effective.cpu.cores} core(s), type={effective.cpu.cpu_type}"
    )
    memory_line = (
        f"Memory: {effective.memory.memory_mib}MiB / balloon minimum "
        f"{effective.memory.min_memory_mib if effective.memory.ballooning else 0}MiB"
    )
    lines = [
        f"Node: {effective.general.node}",
        f"VM ID: {effective.general.vmid}",
        f"Name: {effective.general.name}",
        f"Pool: {effective.general.pool or '-'}",
        f"Tags: {', '.join(effective.general.tags) or '-'}",
        f"HA: {'enabled' if effective.general.ha_enabled else 'disabled'}",
        f"On boot: {'enabled' if effective.general.onboot else 'disabled'}",
        f"Startup: {startup or '-'}",
        "",
        f"Media: {effective.os.media_choice}",
        f"ISO storage: {effective.os.storage or '-'}",
        f"ISO: {effective.os.iso or '-'}",
        f"Guest: {effective.os.guest_type} / {effective.os.guest_version}",
        "",
        system_line,
        efi_line,
        f"TPM: {'enabled' if effective.system.tpm_enabled else 'disabled'}",
        f"Qemu agent: {'enabled' if effective.system.qemu_agent else 'disabled'}",
        "",
        "Disks:",
    ]
    for disk in effective.disks:
        lines.append(
            f"  - {disk.slot_name}: {disk.storage} {disk.size_gib}GiB "
            f"cache={disk.cache} discard={'on' if disk.discard else 'ignore'}"
        )
    lines.extend(
        [
            "",
            cpu_line,
            memory_line,
            "",
            (
                "Network: disabled"
                if effective.network.no_network_device
                else f"Network: {effective.network.model} on {effective.network.bridge}"
            ),
        ]
    )
    return "\n".join(lines)
