import enum
from typing import Iterable, Self, TypedDict

from ..config import GlobalConfig
from ..i18n import _
from ..utils.porcelain import PorcelainEntity, PorcelainEntityType
from .distfile import Distfile
from .host import canonicalize_host_str, get_native_host
from .list_filter import ListFilter
from .pkg_manifest import BoundPackageManifest, PackageManifestType
from .composite_repo import CompositeRepo


class PkgRemark(enum.StrEnum):
    Latest = "latest"
    LatestPreRelease = "latest-prerelease"
    NoBinaryForCurrentHost = "no-binary-for-current-host"
    PreRelease = "prerelease"
    HasKnownIssue = "known-issue"
    Downloaded = "downloaded"
    Installed = "installed"

    def as_rich_markup(self) -> str:
        match self:
            case self.Latest:
                return _("latest")
            case self.LatestPreRelease:
                return _("latest-prerelease")
            case self.NoBinaryForCurrentHost:
                return _("[red]no binary for current host[/]")
            case self.PreRelease:
                return _("prerelease")
            case self.HasKnownIssue:
                return _("[yellow]has known issue[/]")
            case self.Downloaded:
                return _("[green]downloaded[/]")
            case self.Installed:
                return _("[green]installed[/]")
        return ""


class AugmentedPkgManifest:
    def __init__(
        self,
        pm: BoundPackageManifest,
        remarks: list[PkgRemark],
        download_size_host_bytes: int | None,
        download_size_host: str | None,
        install_size: int | None = None,
    ) -> None:
        self.pm = pm
        self.remarks = remarks
        self.download_size_host_bytes = download_size_host_bytes
        self.download_size_host = download_size_host
        self.install_size = install_size
        self._is_downloaded = PkgRemark.Downloaded in remarks
        self._is_installed = PkgRemark.Installed in remarks

    def to_porcelain(self) -> "PorcelainPkgVersionV1":
        return {
            "semver": str(self.pm.semver),
            "pm": self.pm.to_raw(),
            "remarks": self.remarks,
            "is_downloaded": self._is_downloaded,
            "is_installed": self._is_installed,
            "download_size_host_bytes": self.download_size_host_bytes,
            "download_size_host": self.download_size_host,
            "install_size": self.install_size,
        }


class AugmentedPkg:
    def __init__(self) -> None:
        self.versions: list[AugmentedPkgManifest] = []

    def add_version(self, v: AugmentedPkgManifest) -> None:
        if self.versions:
            if v.pm.category != self.category or v.pm.name != self.name:
                raise ValueError("cannot add a version of a different pkg")
        self.versions.append(v)

    @property
    def category(self) -> str | None:
        return self.versions[0].pm.category if self.versions else None

    @property
    def name(self) -> str | None:
        return self.versions[0].pm.name if self.versions else None

    @classmethod
    def yield_from_repo(
        cls,
        cfg: GlobalConfig,
        mr: CompositeRepo,
        filters: ListFilter,
    ) -> "Iterable[Self]":
        rgs = cfg.ruyipkg_global_state
        native_host = str(get_native_host())

        for category, pkg_name, pkg_vers in mr.iter_pkgs():
            if not filters.check_pkg_name(cfg, mr, category, pkg_name):
                continue

            pkg = cls()

            semvers = [pm.semver for pm in pkg_vers.values()]
            semvers.sort(reverse=True)
            found_latest = False
            for i, sv in enumerate(semvers):
                # TODO: support filter ops against individual versions

                pm = pkg_vers[str(sv)]

                latest = False
                latest_prerelease = i == 0
                prerelease = pm.is_prerelease
                if not found_latest and not prerelease:
                    latest = True
                    found_latest = True

                remarks: list[PkgRemark] = []
                if latest or latest_prerelease or prerelease:
                    if prerelease:
                        remarks.append(PkgRemark.PreRelease)
                    if latest:
                        remarks.append(PkgRemark.Latest)
                    if latest_prerelease and not latest:
                        remarks.append(PkgRemark.LatestPreRelease)
                if pm.service_level.has_known_issues:
                    remarks.append(PkgRemark.HasKnownIssue)
                if bm := pm.binary_metadata:
                    if not bm.is_available_for_current_host:
                        remarks.append(PkgRemark.NoBinaryForCurrentHost)
                if _is_pkg_fully_downloaded(pm):
                    remarks.append(PkgRemark.Downloaded)

                host = native_host if bm is not None else ""
                is_installed = rgs.is_package_installed(
                    pm.repo_id,
                    pm.category,
                    pm.name,
                    str(sv),
                    host,
                )
                if is_installed:
                    remarks.append(PkgRemark.Installed)

                dl_size = _get_pkg_download_size_for_host(pm, native_host)
                install_size = _get_pkg_install_size(pm, native_host)
                pkg.add_version(
                    AugmentedPkgManifest(
                        pm,
                        remarks,
                        dl_size,
                        native_host if dl_size is not None else None,
                        install_size=install_size,
                    )
                )

            yield pkg

    def to_porcelain(self) -> "PorcelainPkgListOutputV1":
        return {
            "ty": PorcelainEntityType.PkgListOutputV1,
            "category": self.category or "",
            "name": self.name or "",
            "vers": [x.to_porcelain() for x in self.versions],
        }


class PorcelainPkgVersionV1(TypedDict):
    semver: str
    pm: PackageManifestType
    remarks: list[PkgRemark]
    is_downloaded: bool
    is_installed: bool
    download_size_host_bytes: int | None
    download_size_host: str | None
    install_size: int | None


class PorcelainPkgListOutputV1(PorcelainEntity):
    category: str
    name: str
    vers: list[PorcelainPkgVersionV1]


def _is_pkg_fully_downloaded(pm: BoundPackageManifest) -> bool:
    dfs = pm.distfiles
    if not dfs:
        return True

    for df_decl in dfs.values():
        df = Distfile(df_decl, pm.repo)
        if not df.is_downloaded():
            return False

    return True


def _get_pkg_distfile_names_for_host(
    pm: BoundPackageManifest,
    host: str,
) -> list[str] | None:
    if bin_md := pm.binary_metadata:
        return bin_md.get_distfile_names_for_host(host)
    if blob_md := pm.blob_metadata:
        return blob_md.get_distfile_names()
    if src_md := pm.source_metadata:
        return src_md.get_distfile_names_for_host(host)
    return None


def _get_pkg_download_size_for_host(
    pm: BoundPackageManifest,
    host: str,
) -> int | None:
    distfile_names = _get_pkg_distfile_names_for_host(pm, host)
    if distfile_names is None:
        return None

    total = 0
    for name in distfile_names:
        df = pm.distfiles.get(name)
        if df is None:
            return None
        total += df.size
    return total


def _get_pkg_install_size(pm: BoundPackageManifest, host: str) -> int | None:
    """Extract the declared install_size from per-artifact metadata."""
    if bin_md := pm.binary_metadata:
        data = bin_md.data.get(canonicalize_host_str(host))
        if data is not None and (md := data.get("metadata")) is not None:
            return md.get("install_size")
        return None
    if blob_md := pm.blob_metadata:
        if (md := blob_md.metadata) is not None:
            return md.get("install_size")
        return None
    if src_md := pm.source_metadata:
        if (md := src_md.metadata) is not None:
            return md.get("install_size")
        return None
    return None
