"""get_upgrade_path -- Per-package upgrade roadmap for a single customer.

Reads the customer's licenses.json (written by get_customer_licenses)
and the PE cache to produce a detailed upgrade roadmap showing all
intermediate versions and seasonal releases between installed and latest.

If licenses.json is missing or stale, the tool tells the AI to call
get_customer_licenses first. If the PE cache is missing or expired,
the tool returns CACHE_REQUIRED.

No new SOQL queries -- reads entirely from local artifacts + cache.
"""

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

from ..utils import pe_helpers, state

logger = logging.getLogger(__name__)

_CLOUD_SERVICE_PACKAGES = {
    "CSPOFA": "Orchestrator Cloud Service",
    "cscfga": "Configurator Cloud Service",
    "csslm": "Solution Management Cloud Service",
}

TOOL_DEFINITION = {
    "name": "get_upgrade_path",
    "description": (
        "Generate a per-package upgrade roadmap for a customer. "
        "Shows all intermediate versions and seasonal releases between "
        "installed and latest for each package. Reads from the customer's "
        "licenses.json and the PE cache -- no new SOQL queries. "
        "Requires get_customer_licenses to have been called first."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "working_directory": {
                "type": "string",
                "description": "Absolute path to the workspace root directory.",
            },
            "account_name": {
                "type": "string",
                "description": (
                    "Customer account name, as returned by find_customer."
                ),
            },
            "packages": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Optional list of specific package names to analyze "
                    "(PE display names or LMA names). If omitted, analyzes "
                    "all packages that are behind the latest version."
                ),
            },
            "target_release": {
                "type": "string",
                "description": (
                    "Optional target seasonal release to upgrade TO "
                    "(e.g. 'R36', 'R37', 'Summer \\'20'). If provided, "
                    "the roadmap stops at this release instead of going "
                    "all the way to the latest. Useful when the customer "
                    "wants a phased upgrade."
                ),
            },
        },
        "required": ["working_directory", "account_name"],
    },
}


def _resolve_target_release(
    target: str, cached_data: dict[str, Any],
) -> int | None:
    """Resolve a target release name (e.g. 'R36') to its sr_position."""
    for sr in cached_data.get("seasonal_releases", []):
        if sr.get("name", "").lower() == target.lower():
            return sr.get("position")
    return None


def _trim_roadmap_to_target(
    gap: dict[str, Any], target_pos: int,
) -> dict[str, Any]:
    """Filter a roadmap to only include releases up to target_pos."""
    roadmap = gap.get("upgrade_roadmap", [])
    trimmed = [entry for entry in roadmap if entry.get("position", 0) <= target_pos]

    versions_behind = sum(len(e["versions"]) for e in trimmed)
    sr_with_updates = len(trimmed)
    installed_pos = gap.get("installed_sr_position", -1)
    sr_in_range = (target_pos - installed_pos) if installed_pos >= 0 else 0

    pos_to_name: dict[int, str] = {}
    for entry in trimmed:
        pos_to_name[entry["position"]] = entry["release"]
    target_sr = pos_to_name.get(target_pos, f"Position {target_pos}")

    gap = dict(gap)
    gap["upgrade_roadmap"] = trimmed
    gap["versions_behind"] = versions_behind
    gap["seasonal_releases_with_updates"] = sr_with_updates
    gap["seasonal_releases_in_range"] = max(0, sr_in_range)
    gap["latest_sr"] = target_sr
    gap["target_release"] = target_sr
    gap["target_position"] = target_pos
    return gap


def _make_slug(name: str) -> str:
    slug = name.lower().strip()
    slug = re.sub(r"[^a-z0-9]+", "-", slug)
    slug = slug.strip("-")
    return slug or "unknown"


def _load_licenses_json(
    working_dir: Path, slug: str,
) -> dict[str, Any] | None:
    path = working_dir / "customers" / slug / "licenses.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text())
    except (json.JSONDecodeError, OSError):
        return None


def _extract_packages_from_licenses(
    licenses_data: dict[str, Any],
    cached_data: dict[str, Any],
) -> list[dict[str, Any]]:
    """Build package list from licenses.json, handling both old and new formats."""
    vcs = licenses_data.get("version_currency_summary")
    if vcs:
        return [
            {
                "pe_name": p.get("package", ""),
                "lma_name": p.get("lma_name") or p.get("package", ""),
                "installed_version": _best_version(p.get("installed_versions", [])),
                "installed_sr": p.get("installed_seasonal_release"),
                "latest_sr": p.get("latest_seasonal_release"),
                "on_latest": p.get("on_latest"),
                "releases_behind": p.get("releases_behind"),
            }
            for p in vcs
        ]

    prod = licenses_data.get("production_licenses", [])
    if not prod:
        return []

    by_pkg: dict[str, list[str]] = {}
    for lic in prod:
        pkg = lic.get("package") or "?"
        ver = lic.get("version") or "?"
        by_pkg.setdefault(pkg, []).append(ver)

    result = []
    for pkg_name, versions in by_pkg.items():
        valid = sorted(set(v for v in versions if v != "?"))
        best = valid[-1] if valid else ""
        ns = pe_helpers.resolve_lma_name_to_namespace(pkg_name, cached_data)
        sr_info = {}
        if ns and best:
            sr_info = pe_helpers.get_installed_and_latest_sr(best, ns, cached_data)
        result.append({
            "pe_name": sr_info.get("pe_name") or pkg_name,
            "lma_name": pkg_name,
            "installed_version": best,
            "installed_sr": sr_info.get("installed_sr"),
            "latest_sr": sr_info.get("latest_sr"),
            "on_latest": sr_info.get("on_latest"),
            "releases_behind": sr_info.get("releases_behind"),
        })
    return result


def _best_version(versions: list[str]) -> str:
    valid = sorted(v for v in versions if v and v != "?")
    return valid[-1] if valid else ""


def _match_filter(pkg: dict[str, Any], filter_names: list[str]) -> bool:
    """Check if package matches any of the user-specified filter names."""
    filter_lower = {n.lower() for n in filter_names}
    return (
        pkg["pe_name"].lower() in filter_lower
        or pkg["lma_name"].lower() in filter_lower
    )


def _format_roadmap(gap: dict[str, Any]) -> str:
    """Format a single package's upgrade roadmap as a text table."""
    roadmap = gap.get("upgrade_roadmap", [])
    if not roadmap:
        return "  No intermediate versions (already current or unknown)."

    lines = []
    lines.append(f"  {'Seasonal Release':<20} {'Versions':<50}")
    lines.append(f"  {'─' * 20} {'─' * 50}")
    for entry in roadmap:
        release = entry["release"]
        versions = ", ".join(
            v["version"] + (" (patch)" if v.get("patch") else "")
            for v in entry["versions"]
        )
        lines.append(f"  {release:<20} {versions}")
    return "\n".join(lines)


def _write_upgrade_json(
    working_dir: Path,
    slug: str,
    account_name: str,
    roadmaps: list[dict[str, Any]],
) -> Path:
    customer_dir = working_dir / "customers" / slug
    customer_dir.mkdir(parents=True, exist_ok=True)
    out_path = customer_dir / "upgrade-path.json"

    out_path.write_text(json.dumps({
        "account_name": account_name,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "packages": roadmaps,
    }, indent=2, default=str))
    return out_path


async def run(arguments: dict[str, Any]) -> str:
    state.set_working_dir(arguments.get("working_directory"))
    working_dir = state.get_working_dir()

    account_name = arguments.get("account_name", "")
    if not account_name:
        return (
            "STATUS: MISSING_CUSTOMER\n\n"
            "account_name is required. Call find_customer first, "
            "then pass the confirmed account_name to this tool."
        )

    filter_packages = arguments.get("packages") or []
    target_release = arguments.get("target_release", "")

    cache_result = pe_helpers.validate_and_load_cache(working_dir)
    if cache_result["status"] != "VALID":
        return (
            f"STATUS: CACHE_REQUIRED\n\n"
            f"PE reference data is {cache_result['status'].lower()}: "
            f"{cache_result.get('reason', 'Unknown')}\n\n"
            f"Call check_compass_data, then load_compass_data to refresh."
        )
    cached_data = cache_result["data"]

    target_pos: int | None = None
    if target_release:
        target_pos = _resolve_target_release(target_release, cached_data)
        if target_pos is None:
            available_srs = sorted(
                (sr.get("name", "") for sr in cached_data.get("seasonal_releases", [])),
            )
            return (
                f"STATUS: INVALID_TARGET\n\n"
                f"Target release '{target_release}' not found in PE data.\n\n"
                f"Available releases: {', '.join(available_srs)}"
            )

    slug = _make_slug(account_name)
    licenses_data = _load_licenses_json(working_dir, slug)

    if licenses_data is None:
        return (
            f"STATUS: LICENSES_REQUIRED\n\n"
            f"No licenses.json found for '{account_name}' "
            f"(expected at customers/{slug}/licenses.json).\n\n"
            f"Call get_customer_licenses first with account_id and "
            f"account_name, then call this tool again."
        )

    generated_at = licenses_data.get("generated_at", "")
    age_hours: float | None = None
    if generated_at:
        try:
            gen_dt = datetime.fromisoformat(generated_at)
            age_hours = (datetime.now(timezone.utc) - gen_dt).total_seconds() / 3600
        except (ValueError, TypeError):
            pass

    if age_hours is not None and age_hours > 24:
        return (
            f"STATUS: LICENSES_STALE\n\n"
            f"licenses.json for '{account_name}' is {age_hours:.0f} hours old "
            f"({age_hours / 24:.0f} days). Upgrade path analysis requires "
            f"fresh data.\n\n"
            f"Call get_customer_licenses first to refresh, then call "
            f"this tool again."
        )

    all_packages = _extract_packages_from_licenses(licenses_data, cached_data)

    if not all_packages:
        return (
            f"STATUS: NO_PACKAGES\n\n"
            f"licenses.json for '{account_name}' contains no production "
            f"packages. The file may be from an older tool version or the "
            f"customer has no production licenses.\n\n"
            f"Run get_customer_licenses again to refresh."
        )

    if filter_packages:
        packages = [p for p in all_packages if _match_filter(p, filter_packages)]
        if not packages:
            available = ", ".join(sorted(p["pe_name"] for p in all_packages))
            return (
                f"STATUS: PACKAGE_NOT_FOUND\n\n"
                f"None of the specified packages ({', '.join(filter_packages)}) "
                f"were found in {account_name}'s production licenses.\n\n"
                f"Available packages: {available}"
            )
    else:
        packages = [p for p in all_packages if not p.get("on_latest", True)]
        if not packages:
            return (
                f"STATUS: ALL_CURRENT\n\n"
                f"All {len(all_packages)} packages for '{account_name}' are "
                f"on the latest version. No upgrade path needed."
            )

    roadmap_results = []
    output_sections = []

    for pkg in packages:
        lma_name = pkg["lma_name"]
        installed = pkg["installed_version"]

        ns = pe_helpers.resolve_lma_name_to_namespace(lma_name, cached_data)
        if not ns:
            roadmap_results.append({
                "package": pkg["pe_name"],
                "lma_name": lma_name,
                "status": "UNMAPPED",
                "note": "Package not found in PE cache. Cannot compute upgrade path.",
            })
            output_sections.append(
                f"\n### {pkg['pe_name']}\n"
                f"  Status: UNMAPPED -- not in PE cache, cannot compute upgrade path."
            )
            continue

        gap = pe_helpers.compute_version_gap(installed, ns, cached_data)

        if target_pos is not None:
            gap = _trim_roadmap_to_target(gap, target_pos)

        cloud_svc = _CLOUD_SERVICE_PACKAGES.get(ns)

        roadmap_entry = {
            "package": gap.get("pe_name") or pkg["pe_name"],
            "lma_name": lma_name,
            "namespace": ns,
            "installed_version": installed,
            "installed_sr": gap.get("installed_sr"),
            "latest_version": gap.get("latest_version"),
            "latest_sr": gap.get("latest_sr"),
            "on_latest": gap.get("on_latest"),
            "versions_behind": gap.get("versions_behind", 0),
            "releases_behind": gap.get("releases_behind"),
            "seasonal_releases_with_updates": gap.get("seasonal_releases_with_updates", 0),
            "seasonal_releases_in_range": gap.get("seasonal_releases_in_range", 0),
            "upgrade_roadmap": gap.get("upgrade_roadmap", []),
        }
        if cloud_svc:
            roadmap_entry["cloud_service"] = cloud_svc
        roadmap_results.append(roadmap_entry)

        section = [f"\n### {roadmap_entry['package']}"]
        section.append(
            f"  Installed: {installed} ({gap.get('installed_sr', '?')}) → "
            f"Latest: {gap.get('latest_version', '?')} ({gap.get('latest_sr', '?')})"
        )
        section.append(
            f"  Gap: {gap.get('versions_behind', 0)} versions across "
            f"{gap.get('seasonal_releases_with_updates', 0)} seasonal releases "
            f"(of {gap.get('seasonal_releases_in_range', 0)} total in range)"
        )
        if cloud_svc:
            section.append(f"  ⚠ Cloud service also needs upgrading: {cloud_svc}")
        section.append("")
        section.append(_format_roadmap(gap))
        output_sections.append("\n".join(section))

    artifact_path = _write_upgrade_json(
        working_dir, slug, account_name, roadmap_results,
    )

    pkg_behind = [r for r in roadmap_results if r.get("versions_behind", 0) > 0]
    total_versions = sum(r.get("versions_behind", 0) for r in pkg_behind)
    cloud_svc_count = sum(1 for r in roadmap_results if r.get("cloud_service"))

    target_label = f" → {target_release}" if target_release else ""
    header = [
        f"STATUS: READY\n",
        f"## Upgrade Path: {account_name}{target_label}\n",
        f"Packages analyzed: {len(packages)}",
        f"Packages behind: {len(pkg_behind)}",
        f"Total intermediate versions: {total_versions}",
    ]
    if target_release:
        header.append(f"Target release: {target_release}")
    if cloud_svc_count:
        header.append(f"Packages with cloud services: {cloud_svc_count}")

    to_sr = target_release or None
    if not to_sr:
        for pkg in roadmap_results:
            lsr = pkg.get("latest_sr")
            if lsr and lsr != "Unknown":
                to_sr = lsr
                break

    footer = [
        f"\n---",
        f"Artifact: {artifact_path}",
        f"\nPresent the roadmap to the user. Highlight:",
        f"- Packages with the largest version gap (most urgent)",
        f"- Cloud service upgrade implications (if any)",
        f"- Suggest grouping upgrades by seasonal release for phased rollout",
        f"- Recommend sandbox testing before production upgrade",
    ]
    if to_sr:
        footer.append(
            f"\nOffer: \"Would you like me to retrieve the release notes and "
            f"user guides for this upgrade to {to_sr}?\"\n"
            f"If yes, call get_release_info with "
            f"to_release='{to_sr}', account_name='{account_name}'. "
            f"No from_release needed -- the tool reads each package's "
            f"installed release from licenses.json automatically."
        )

    return "\n".join(header + output_sections + footer)
