import dataclasses
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional

from ..async_client import NetlabClient
from ..enums import VirtualMachineRole
from . import pod_op, schema
from .schema import BuilderPCCloneSpec, ThrottlerConfig
from .types import ExistingPod, NetlabHost

log = logging.getLogger(__name__)


def client_factory_from_manifest(manifest: schema.Manifest) -> NetlabClient:
    if manifest.system is None:
        client_fact = NetlabClient()
    elif isinstance(manifest.system, schema.SystemSpec):
        client_fact = NetlabClient(config=manifest.system.config)
    elif isinstance(manifest.system, schema.SystemSpec1):
        client_fact = NetlabClient(system=manifest.system.name)
    else:
        client_fact = NetlabClient(config=manifest.system)

    return client_fact


@dataclass
class CompiledDeletePod:
    pod_id: int
    parent: "CompiledDelete"

    existing_pod: ExistingPod | None = None
    superseded_by: Optional["CompiledPod"] = None

    def get_conflicts(self) -> list[tuple[str, Any, Any]]:
        if self.existing_pod is not None:
            return [("state", "ABSENT", "EXISTS")]
        else:
            return []

    def pod_op(self) -> pod_op.PodOp:
        if self.superseded_by is not None:
            return pod_op.Superceded(pod=self, by=self.superseded_by)
        elif self.existing_pod is not None:
            return pod_op.Delete(pod=self, existing=self.existing_pod)
        else:
            return pod_op.Absent(pod=self)
        # conflicts = pod.get_conflicts()


@dataclass
class CompiledPod:
    pod_id: int
    parent: "CompiledRange"

    existing_pod: ExistingPod | None = None

    def get_host(self) -> NetlabHost | None:
        return self.parent.host

    def get_role(self) -> VirtualMachineRole:
        return self.parent.get_role()

    def get_pod_type(self) -> str:
        assert self.parent.source_pod
        return self.parent.source_pod.get_pod_type()

    def pod_op(self) -> pod_op.PodOp:
        if self.existing_pod is None:
            return pod_op.Create(
                pod=self,
            )
        else:
            conflicts = [
                ("pod_name", self.get_name(), self.existing_pod.get_name()),
                ("pt_id", self.parent.get_pod_type(), self.existing_pod.current_pod_type),
                ("vh_host", self.parent.host, self.existing_pod.current_host),
                ("vm_role", self.parent.get_role(), self.existing_pod.current_role),
            ]
            conflicts = list(filter(lambda x: x[1] != x[2], conflicts))

            if len(conflicts) == 0:
                return pod_op.Present(pod=self, existing=self.existing_pod)
            else:
                return pod_op.Rebuild(
                    pod=self,
                    existing=self.existing_pod,
                    conflicts=conflicts,
                )

    # def get_conflicts(self) -> list[tuple[str, Any, Any]]:
    #     if self.existing_pod is None:
    #         return [("state", "EXISTS", "ABSENT")]
    #
    #     results = [
    #         ("pod_name", self.get_name(), self.existing_pod.get_name()),
    #         ("pt_id", self.parent.get_pod_type(), self.existing_pod.current_pod_type),
    #         ("vh_host", self.parent.host, self.existing_pod.current_host),
    #         ("vm_role", self.parent.get_role(), self.existing_pod.current_role),
    #     ]
    #
    #     return list(filter(lambda x: x[1] != x[2], results))

    def get_name(self) -> str:
        vh = self.parent.host
        assert vh
        vh_id, vh_name = vh
        ip_split = re.split(r"[.-]", vh_name)
        host_suffix = "H" + ip_split[-1]

        match self.parent.get_role():
            case VirtualMachineRole.MASTER:
                pod_kind = "M"
            case VirtualMachineRole.NORMAL | VirtualMachineRole.PERSISTENT:
                pod_kind = "P"
            case VirtualMachineRole.TEMPLATE:
                pod_kind = "T"

        clone_name = f"{host_suffix}.{pod_kind}{self.pod_id}"
        if self.parent.clone_prefix is not None:
            clone_name = self.parent.clone_prefix + "." + clone_name
        if self.parent.clone_suffix is not None:
            clone_name = clone_name + "." + self.parent.clone_suffix

        context = {
            "pod_id": self.pod_id,
            "pod_role": pod_kind,
            "vh_name": ip_split[-1],
            "pt_org": self.parent.get_existing_pod().pt_org,
            "pt_apdid": self.parent.get_existing_pod().pt_apdid,
            "default": clone_name,
        }

        return self.parent.clone_name_template.format(**context)


@dataclass
class CompiledDelete:
    pods: list[CompiledDeletePod]


@dataclass
class CompiledRangeErrors:
    errors: list[tuple[str, str]]


@dataclass
class CompiledRange:
    pod_online: bool
    source_pod: ExistingPod | CompiledPod
    pc_clone_specs: schema.BuilderPCCloneSpec | None
    clone_prefix: str | None
    clone_suffix: str | None
    clone_name_template: str

    pods: list[CompiledPod] = dataclasses.field(default_factory=list)
    host: NetlabHost | None = None

    def get_existing_pod(self) -> ExistingPod:
        match self.source_pod:
            case ExistingPod():
                return self.source_pod
            case CompiledPod():
                return self.source_pod.parent.get_existing_pod()

    def get_pod_type(self) -> str:
        assert self.source_pod is not None
        return self.source_pod.get_pod_type()

    def get_role(self) -> VirtualMachineRole:
        if self.pc_clone_specs is not None:
            clone_role = self.pc_clone_specs.get("clone_role", "NORMAL")
            return VirtualMachineRole[clone_role]
        else:
            return VirtualMachineRole.NORMAL

    # def get_host(self) -> NetlabHost | None:
    #     if self.host is not None:
    #         return self.host
    #     else:
    #         return self.source_pod.get_host()


def _host_from_pod(
    host_map: dict[int | str, NetlabHost], pc_clone_specs: schema.BuilderPCCloneSpec | None
) -> NetlabHost | None:
    if pc_clone_specs is None:
        host = None
        pc_clone_specs = None
    else:
        clone_vh_id = pc_clone_specs.get("clone_vh_id")
        clone_vh_name = pc_clone_specs.get("clone_vh_name")

        match (clone_vh_id, clone_vh_name):
            case (None, None):
                host = None
            case (int(clone_vh_id), None):
                host = host_map[clone_vh_id]
            case (None, str(clone_vh_name)):
                host = host_map[clone_vh_name]
            case (int(clone_vh_id), str(clone_vh_name)):
                host = host_map[clone_vh_name]
                assert host.vh_id == clone_vh_id
                assert host.vh_name == clone_vh_name
    return host


@dataclass
class CompilerError(Exception):
    code: str
    desc: str


@dataclass
class CompiledManifest:
    manifest_path: Path
    client: NetlabClient
    throttle_delete: ThrottlerConfig
    throttle_create: ThrottlerConfig
    throttle_updown: ThrottlerConfig
    ranges: list[CompiledRange | CompiledDelete | CompiledRangeErrors] = dataclasses.field(default_factory=list)

    def _compile_delete_range(
        self,
        existing_pods: dict[int, ExistingPod],
        compiled_pods: dict[int, CompiledPod],
        compiled_delete_pods: dict[int, CompiledDeletePod],
        r,
    ):
        compiled_delete_range = CompiledDelete(pods=[])
        self.ranges.append(compiled_delete_range)
        for p in r.get_range():
            compiled_delete_pod = CompiledDeletePod(
                p,
                existing_pod=existing_pods.get(p),
                parent=compiled_delete_range,
            )
            assert compiled_delete_pod.pod_id not in compiled_pods
            compiled_delete_pod = compiled_delete_pods.setdefault(p, compiled_delete_pod)
            compiled_delete_range.pods.append(compiled_delete_pod)

    def _compile_range(self):
        pass

    @classmethod
    def from_schema(
        cls,
        manifest: schema.Manifest,
        manifest_path: Path,
        host_map: dict[int | str, NetlabHost],
        existing_pods: dict[int, ExistingPod],
    ) -> "CompiledManifest":
        compiled_manifest = CompiledManifest(
            manifest_path=manifest_path,
            client=client_factory_from_manifest(manifest),
            throttle_create=manifest.throttles.create,
            throttle_delete=manifest.throttles.delete,
            throttle_updown=manifest.throttles.updown,
        )
        compiled_pods: dict[int, CompiledPod] = {}
        compiled_delete_pods: dict[int, CompiledDeletePod] = {}

        for r in manifest.range:
            if r.delete:
                compiled_manifest._compile_delete_range(
                    existing_pods,
                    compiled_pods,
                    compiled_delete_pods,
                    r,
                )
                continue
            else:
                pass

            errors = []
            pc_clone_specs: BuilderPCCloneSpec = {}

            source_pod = compiled_pods.get(r.source_pod_id, existing_pods.get(r.source_pod_id))
            if source_pod is None:
                errors.append(("NO_SOURCE_POD", ""))
            else:
                try:
                    if r.pc_clone_specs:
                        pc_clone_specs.update(r.pc_clone_specs)

                    host = _host_from_pod(host_map, pc_clone_specs)
                    if host is None:
                        host = source_pod.get_host()

                    if host is None:
                        errors.append(("NO_CLONE_HOST", "The host to clone to could not be determined."))
                    else:
                        pc_clone_specs["clone_vh_id"] = host.vh_id
                        pc_clone_specs.pop("clone_vh_name", None)  # TODO
                except KeyError as e:
                    errors.append(("HOST_NOT_FOUND", f"Host '{e.args[0]}' not found."))

                if (
                    source_pod.get_role() is not VirtualMachineRole.TEMPLATE
                    and pc_clone_specs
                    and pc_clone_specs.get("clone_type") != "FULL"
                ):
                    errors.append(
                        (
                            "FULL_CLONE_REQUIRED",
                            f"Source pod {source_pod.pod_id} is not a TEMPLATE so a FULL clone is required.",
                        )
                    )

            if errors:
                compiled_manifest.ranges.append(CompiledRangeErrors(errors=errors))
                continue
            else:
                assert source_pod is not None

            compiled_range = CompiledRange(
                pod_online=r.pod_online,
                pc_clone_specs=pc_clone_specs,
                host=host,
                clone_prefix=r.clone_prefix if r.clone_prefix is not False else None,
                clone_suffix=r.clone_suffix if r.clone_suffix is not False else None,
                clone_name_template=r.clone_name_template,
                source_pod=source_pod,
            )
            compiled_manifest.ranges.append(compiled_range)

            for p in r.get_range():
                compiled_pod = CompiledPod(
                    p,
                    existing_pod=existing_pods.get(p),
                    parent=compiled_range,
                )
                compiled_pod = compiled_pods.setdefault(p, compiled_pod)
                if compiled_pod.pod_id in compiled_delete_pods:
                    compiled_delete_pods[compiled_pod.pod_id].superseded_by = compiled_pod
                compiled_range.pods.append(compiled_pod)

        return compiled_manifest
