"""PE (Packaging Environment) helpers -- read-only functions over cached data.

This module provides:
  - PRE_R33_RELEASE_ORDER: hardcoded ordering for historical seasonal releases
  - DEFAULT_NAMESPACE_MAPPING: maps LMA package names <-> PE names <-> namespaces
  - Functions to read cached reference data and look up version currency

Ordering strategy (hybrid):
  - Pre-R33 releases were bulk-loaded into the PE database on the same day
    (2020-05-25) with unreliable External_ID ordering, so their order is
    hardcoded here (14 entries, will never change).
  - R33+ releases were created individually with reliable CreatedDate
    timestamps. Their order is derived dynamically by load_compass_data
    from the org data. New releases (R38, R39, ...) are discovered
    automatically without code changes.

All runtime ordering uses sr_position from the cached reference data.
"""

import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

CACHE_DIR = ".cache"
REFERENCE_DATA_FILE = "reference-data.json"
CACHE_META_FILE = "cache-meta.json"
DEFAULT_TTL_HOURS = 24

PRE_R33_RELEASE_ORDER: list[str] = [
    "Winter '16",
    "Summer '16",
    "Winter '17",
    "Spring '17",
    "Summer '17",
    "Winter '18",
    "Spring '18",
    "Summer '18",
    "Winter '19",
    "Spring '19",
    "Summer '19",
    "Winter '20",
    "Spring '20",
    "Summer '20",
]

PRE_R33_POSITION: dict[str, int] = {
    name: idx for idx, name in enumerate(PRE_R33_RELEASE_ORDER)
}

DEFAULT_NAMESPACE_MAPPING: list[dict[str, str]] = [
    {"namespace": "cscfga",         "lma_name": "CloudSense Configurator",                              "pe_name": "CloudSense CPQ (Configurator)"},
    {"namespace": "CSPOFA",         "lma_name": "Process Orchestration Framework",                      "pe_name": "Orchestrator"},
    {"namespace": "csbb",           "lma_name": "CloudSense Basket Builder",                            "pe_name": "CloudSense Sales Console (BB)"},
    {"namespace": "csord",          "lma_name": "Orders and Subscriptions Object Model",                "pe_name": "CloudSense Order Management"},
    {"namespace": "cspmb",          "lma_name": "Product and Pricing Data Model",                       "pe_name": "Product & Pricing Data Model"},
    {"namespace": "CSCAP",          "lma_name": "CloudSense Click Approve",                             "pe_name": "CloudSense Click Approve"},
    {"namespace": "csam",           "lma_name": "CloudSense Messaging",                                 "pe_name": "CloudSense Messaging"},
    {"namespace": "csordcb",        "lma_name": "CloudSense Order Codebase",                            "pe_name": "CloudSense Order Codebase"},
    {"namespace": "csconta",        "lma_name": "CloudSense Contracts",                                 "pe_name": "CloudSense Contracts"},
    {"namespace": "csclm",          "lma_name": "CloudSense Contract Lifecycle Management Data Model",  "pe_name": "CloudSense CLM Data Model"},
    {"namespace": "csclmcb",        "lma_name": "CloudSense Contract Lifecycle Management",             "pe_name": "CloudSense CLM"},
    {"namespace": "csclmconga",     "lma_name": "CloudSense CLM Conga Plugin",                          "pe_name": "CloudSense CLM Conga Plugin"},
    {"namespace": "csclmdg",        "lma_name": "Contract Authoring and Negotiation Document Generation plugin", "pe_name": "CloudSense CLM DG Plugin"},
    {"namespace": "csdf",           "lma_name": "CloudSense Digital Fulfilment",                        "pe_name": "CloudSense Digital Fulfilment"},
    {"namespace": "csb2c",          "lma_name": "CloudSense E-Commerce",                                "pe_name": "CloudSense E-Commerce"},
    {"namespace": "csinv",          "lma_name": "CloudSense Invoicer",                                  "pe_name": "CloudSense Invoicer"},
    {"namespace": "csinva",         "lma_name": "CloudSense Invoicing",                                 "pe_name": "CloudSense Invoicing"},
    {"namespace": "csac",           "lma_name": "CloudSense Anywhere Connector",                        "pe_name": "CloudSense Anywhere Connector"},
    {"namespace": "csordm",         "lma_name": "CloudSense Order",                                     "pe_name": "CloudSense Order"},
    {"namespace": "csmso",          "lma_name": "Cloud Sense Media Sales Order",                        "pe_name": "CloudSense Media Sales Order"},
    {"namespace": "cssvc1",         "lma_name": "CloudSense Service Console",                           "pe_name": "CloudSense Service Console"},
    {"namespace": "csibt",          "lma_name": "CloudSense Inventory Booking Tool",                    "pe_name": "CloudSense Inventory Booking Tool"},
    {"namespace": "cstelcobb",      "lma_name": "CloudSense Telecoms Module Basket Builder Link",       "pe_name": "CloudSense Telecoms BB Link"},
    {"namespace": "csordtelcoa",    "lma_name": "CS Order Implementation Module Telco A",               "pe_name": "CS Order Telco A"},
    {"namespace": "csordmedia",     "lma_name": "CS Order Implementation Module Media",                 "pe_name": "CS Order Media"},
    {"namespace": "csbbta3",        "lma_name": "CSBB TA3",                                             "pe_name": "CSBB TA3"},
    {"namespace": "csutil",         "lma_name": "CS Utilities",                                         "pe_name": "CS Utilities"},
    {"namespace": "cstst",          "lma_name": "CloudSense Testing Automation Tool",                   "pe_name": "CloudSense Testing Automation Tool"},
    {"namespace": "csog",           "lma_name": "ObjectGraph",                                          "pe_name": "ObjectGraph"},
    {"namespace": "cscfglint",      "lma_name": "CS Configurator Lint",                                 "pe_name": "CS Configurator Lint"},
    {"namespace": "cscfga_t1",      "lma_name": "CloudSense Configurator Test-1",                       "pe_name": "CloudSense Configurator Test-1"},
    {"namespace": "cscfgc",         "lma_name": "OMS Configurator",                                     "pe_name": "OMS Configurator"},
    {"namespace": "csexpimp1",      "lma_name": "CSOM Export Import",                                   "pe_name": "CSOM Export Import"},
    {"namespace": "CSORD_CB_demo02","lma_name": "CSORD Codebase",                                       "pe_name": "CSORD Codebase Demo"},
    {"namespace": "cscrm",          "lma_name": "CRM Extensions",                                       "pe_name": "CRM Extensions"},
    {"namespace": "cspdui",         "lma_name": "Product Design Studio",                                "pe_name": "Product Design Studio"},
    {"namespace": "cspduia",        "lma_name": "CloudSense Product Explorer & Designer",               "pe_name": "Product Explorer & Designer"},
    {"namespace": "csoe",           "lma_name": "Order Enrichment",                                     "pe_name": "Order Enrichment"},
    {"namespace": "csedm",          "lma_name": "External Data Mapping",                                "pe_name": "External Data Mapping"},
    {"namespace": "csdiscounts",    "lma_name": "Discounts",                                            "pe_name": "Discounts"},
    {"namespace": "cspl",           "lma_name": "P&L",                                                  "pe_name": "P&L"},
    {"namespace": "csmle",          "lma_name": "Order Enrichment Dependencies",                        "pe_name": "Order Enrichment Dependencies"},
    {"namespace": "csmediabb",      "lma_name": "Orders and Subscriptions Media Link Package",          "pe_name": "O&S Media Link"},
    {"namespace": "cssolution_t1",  "lma_name": "CS Solutions Console T1",                              "pe_name": "CS Solutions Console T1"},
    {"namespace": "cssmgnt",        "lma_name": "CloudSense Solution Management",                       "pe_name": "CloudSense Solution Management"},
    {"namespace": "csfam",          "lma_name": "Framework Agreement Management",                       "pe_name": "Frame Agreement Management"},
    {"namespace": "csrcm",          "lma_name": "Rate Card Manager",                                    "pe_name": "Rate Card Manager"},
    {"namespace": "csaom",          "lma_name": "Add-On Manager",                                       "pe_name": "Add-On Manager"},
    {"namespace": "cssdm",          "lma_name": "Solution Management Object Model",                     "pe_name": "Solution Management Object Model"},
    {"namespace": "csdg",           "lma_name": "CloudSense Creator",                                   "pe_name": "CloudSense Creator"},
    {"namespace": "cfgug1",         "lma_name": "Configuration Upgrade Utility",                        "pe_name": "Configuration Upgrade Utility"},
    {"namespace": "csmm",           "lma_name": "Bulk Subscriber Management",                           "pe_name": "Bulk Subscriber Management"},
    {"namespace": "cspsi",          "lma_name": "Advanced Pricing Integration",                         "pe_name": "Advanced Pricing Integration"},
    {"namespace": "csjil",          "lma_name": "Inline Editor",                                        "pe_name": "Inline Editor"},
    {"namespace": "csfamext",       "lma_name": "Frame Agreement Manager Extensions",                   "pe_name": "Frame Agreement Manager Extensions"},
    {"namespace": "csplm",          "lma_name": "Product Lifecycle Management",                         "pe_name": "Product Lifecycle Management"},
    {"namespace": "csordnav",       "lma_name": "Orders and Subscriptions Navigator",                   "pe_name": "O&S Navigator"},
    {"namespace": "CS_GT",          "lma_name": "CloudSense_GroupTasks",                                "pe_name": "CloudSense GroupTasks"},
    {"namespace": "CSSX",           "lma_name": "CS Scheduler",                                         "pe_name": "CS Scheduler"},
    {"namespace": "cfgoffline",     "lma_name": "CloudSense Configurator Offline Support",              "pe_name": "Configurator Offline Support"},
    {"namespace": "cfgoffline_t2",  "lma_name": "(Test2) CS Configurator - Offline Support",            "pe_name": "Configurator Offline Support Test-2"},
    {"namespace": "csslm",          "lma_name": "CloudSense Solution Management",                       "pe_name": "Solution Management"},
    {"namespace": "cpra1",          "lma_name": "CloudSense Cross Product Rules",                       "pe_name": "CloudSense Cross Product Rules"},
    {"namespace": "CSPRS",          "lma_name": "CloudSense Pricing Service",                           "pe_name": "CloudSense Pricing Service"},
    {"namespace": "cscommapi",      "lma_name": "CloudSense Commerce APIs",                             "pe_name": "CloudSense Commerce APIs"},
    {"namespace": "ccetl",          "lma_name": "CloudSense Commerce Cloud ETL",                        "pe_name": "CloudSense Commerce Cloud ETL"},
]

_LMA_NAME_TO_NS: dict[str, str] = {
    entry["lma_name"]: entry["namespace"] for entry in DEFAULT_NAMESPACE_MAPPING
}


def validate_and_load_cache(
    working_dir: Path | str,
) -> dict[str, Any]:
    """Validate cache freshness (TTL) and load reference data if valid.

    Returns a dict with:
        status: "VALID" | "STALE" | "MISSING"
        data: parsed reference data (only when VALID)
        reason: human-readable explanation (when STALE or MISSING)
        age_hours, cached_at, org_alias, ttl_hours: meta details (when available)
    """
    wd = Path(working_dir)
    meta_path = wd / CACHE_DIR / CACHE_META_FILE

    if not meta_path.exists():
        return {"status": "MISSING", "reason": "No reference data cache found."}

    try:
        meta = json.loads(meta_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {"status": "MISSING", "reason": "Cache meta file is unreadable."}

    cached_at_str = meta.get("cached_at")
    if not cached_at_str:
        return {"status": "MISSING", "reason": "Cache meta is missing the cached_at timestamp."}

    try:
        cached_at = datetime.fromisoformat(cached_at_str)
    except (ValueError, TypeError):
        return {"status": "MISSING", "reason": "Cache meta has an invalid timestamp."}

    ttl_hours = meta.get("ttl_hours", DEFAULT_TTL_HOURS)
    age_hours = (datetime.now(timezone.utc) - cached_at).total_seconds() / 3600
    org_alias = meta.get("org_alias", "unknown")

    details = {
        "age_hours": age_hours,
        "cached_at": cached_at_str,
        "org_alias": org_alias,
        "ttl_hours": ttl_hours,
    }

    if age_hours > ttl_hours:
        return {
            **details,
            "status": "STALE",
            "reason": f"Cache expired ({age_hours:.1f}h old, TTL is {ttl_hours}h).",
        }

    data_path = wd / CACHE_DIR / REFERENCE_DATA_FILE
    if not data_path.exists():
        return {
            **details,
            "status": "MISSING",
            "reason": "Cache meta is valid but reference-data.json is missing.",
        }

    try:
        data = json.loads(data_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {
            **details,
            "status": "MISSING",
            "reason": "Cache meta is valid but reference-data.json is corrupt.",
        }

    return {**details, "status": "VALID", "data": data}


def resolve_lma_name_to_namespace(
    lma_package_name: str, cached_data: dict[str, Any],
) -> str | None:
    """Map an LMA Package_Name__c to its namespace.

    Tries the cached packages dict first (enriched with pe_id at runtime),
    then falls back to the shipped DEFAULT_NAMESPACE_MAPPING constant.
    """
    packages = cached_data.get("packages", {})
    for ns, pkg_info in packages.items():
        if pkg_info.get("lma_name") == lma_package_name:
            return ns
    return _LMA_NAME_TO_NS.get(lma_package_name)


def get_installed_and_latest_sr(
    installed_version: str,
    namespace: str,
    cached_data: dict[str, Any],
) -> dict[str, Any]:
    """Lightweight SR lookup for get_customer_licenses / get_portfolio_data.

    Returns:
        {
            "installed_sr": "Summer '18" | "Unknown",
            "latest_sr": "R37",
            "latest_version": "37.0",
            "on_latest": bool,
            "releases_behind": 15,
            "pe_name": "CloudSense CPQ (Configurator)",
        }
    """
    versions = cached_data.get("versions", {}).get(namespace, [])
    pkg_info = cached_data.get("packages", {}).get(namespace, {})
    pe_name = pkg_info.get("pe_name", namespace)

    installed_sr = "Unknown"
    installed_pos = -1

    if versions and installed_version:
        match = _find_version(installed_version, versions)
        if match:
            installed_sr = match["seasonal_release"]
            installed_pos = match.get("sr_position", -1)

    active_versions = [v for v in versions if not v.get("withdrawn", False)]
    latest_sr = "Unknown"
    latest_version = "Unknown"
    latest_pos = -1

    if active_versions:
        best = max(active_versions, key=lambda v: v.get("sr_position", -1))
        latest_sr = best["seasonal_release"]
        latest_version = best["version"]
        latest_pos = best.get("sr_position", -1)

    on_latest = installed_pos >= 0 and installed_pos >= latest_pos

    releases_behind: int | None = None
    if installed_pos >= 0 and not on_latest:
        sr_positions_ahead = {
            v.get("sr_position") for v in active_versions
            if v.get("sr_position", -1) > installed_pos
        }
        releases_behind = len(sr_positions_ahead)
    elif on_latest:
        releases_behind = 0

    return {
        "installed_sr": installed_sr,
        "latest_sr": latest_sr,
        "latest_version": latest_version,
        "on_latest": on_latest,
        "releases_behind": releases_behind,
        "pe_name": pe_name,
    }


def compute_version_gap(
    installed_version: str,
    namespace: str,
    cached_data: dict[str, Any],
) -> dict[str, Any]:
    """Deep analysis for get_upgrade_path.

    Returns everything from get_installed_and_latest_sr() plus:
      - versions_behind: count of non-withdrawn versions after installed
      - seasonal_releases_with_updates: SRs where this package had a version
      - seasonal_releases_in_range: total SRs between installed and latest
      - upgrade_roadmap: versions grouped by SR, ordered by position
    """
    base = get_installed_and_latest_sr(installed_version, namespace, cached_data)
    versions = cached_data.get("versions", {}).get(namespace, [])

    installed_pos = -1
    if installed_version:
        match = _find_version(installed_version, versions)
        if match:
            installed_pos = match.get("sr_position", -1)

    base["installed_sr_position"] = installed_pos

    latest_pos = -1
    active_versions = [v for v in versions if not v.get("withdrawn", False)]
    if active_versions:
        latest_pos = max(v.get("sr_position", -1) for v in active_versions)
    base["latest_sr_position"] = latest_pos

    if installed_pos < 0 or latest_pos < 0 or installed_pos >= latest_pos:
        base["versions_behind"] = 0
        base["seasonal_releases_with_updates"] = 0
        base["seasonal_releases_in_range"] = 0
        base["upgrade_roadmap"] = []
        return base

    ahead = [
        v for v in active_versions
        if v.get("sr_position", -1) > installed_pos
    ]
    base["versions_behind"] = len(ahead)

    sr_positions_with_updates = {v.get("sr_position") for v in ahead}
    base["seasonal_releases_with_updates"] = len(sr_positions_with_updates)

    range_positions = set(range(installed_pos + 1, latest_pos + 1))
    base["seasonal_releases_in_range"] = len(range_positions)

    pos_to_name: dict[int, str] = {
        sr["position"]: sr["name"]
        for sr in cached_data.get("seasonal_releases", [])
    }

    roadmap: list[dict[str, Any]] = []
    by_sr: dict[int, list[dict[str, Any]]] = {}
    for v in ahead:
        pos = v.get("sr_position", -1)
        by_sr.setdefault(pos, []).append(v)

    for pos in sorted(by_sr.keys()):
        sr_name = pos_to_name.get(pos, f"Position {pos}")
        vlist = sorted(by_sr[pos], key=lambda x: x.get("version", ""))
        roadmap.append({
            "release": sr_name,
            "position": pos,
            "versions": [
                {
                    "version": v["version"],
                    "patch": v.get("patch", False),
                    "withdrawn": v.get("withdrawn", False),
                }
                for v in vlist
            ],
        })
    base["upgrade_roadmap"] = roadmap

    return base


def _parse_version_tuple(ver_str: str) -> tuple[int, ...]:
    """Parse a dot-separated version string into a comparable tuple."""
    parts: list[int] = []
    for p in ver_str.split("."):
        try:
            parts.append(int(p))
        except ValueError:
            parts.append(0)
    return tuple(parts)


def _find_version(
    version_str: str, versions: list[dict[str, Any]],
) -> dict[str, Any] | None:
    """Find a version record by exact match, prefix match, or nearest lower neighbor."""
    for v in versions:
        if v.get("version") == version_str:
            return v

    for v in versions:
        pv = v.get("version", "")
        if pv and version_str.startswith(pv):
            return v
        if pv and pv.startswith(version_str):
            return v

    major = version_str.split(".")[0]
    same_major = [
        v for v in versions
        if v.get("version", "").split(".")[0] == major
    ]
    if same_major:
        target = _parse_version_tuple(version_str)
        candidates = []
        for v in same_major:
            pv = _parse_version_tuple(v.get("version", ""))
            if pv <= target:
                candidates.append((pv, v))
        if candidates:
            candidates.sort(key=lambda x: x[0])
            return candidates[-1][1]
        same_major.sort(key=lambda v: _parse_version_tuple(v.get("version", "")))
        return same_major[0]

    return None
