"""get_release_info -- Retrieve release notes and user guides from PE.

Queries Document_Reference_Association__c in PE to find Google Drive
links for release notes, user guides, and other documents associated
with package versions in a seasonal release range.

Two modes:

  Customer mode (account_name provided):
    Reads licenses.json to determine each package's installed seasonal
    release. Uses that as the per-package start point -- no global
    from_release needed. Only to_release (target) is required.
    Writes under customers/<slug>/release-docs/.

  Browse mode (no account_name):
    Requires both from_release and to_release. All packages (or
    filtered by packages param) in that range.
    Writes under release-docs/.

Documents are organised by package -> release -> doc type. Each .md
file contains the document title and Google Drive URL. User guides
are only included at the to_release (target) version, not intermediates.

Folder creation is additive: re-running for a different range adds
new folders without deleting existing ones.
"""

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

from ..utils import pe_helpers, sf_cli, state

logger = logging.getLogger(__name__)

TOOL_DEFINITION = {
    "name": "get_release_info",
    "description": (
        "Retrieve release notes and user guides for CloudSense packages "
        "across a range of seasonal releases. Returns Google Drive links "
        "organised by package and release. "
        "Customer mode (account_name): reads each package's installed "
        "release from licenses.json -- only to_release is needed. "
        "Browse mode: requires both from_release and to_release."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "working_directory": {
                "type": "string",
                "description": "Absolute path to the workspace root directory.",
            },
            "to_release": {
                "type": "string",
                "description": (
                    "Target seasonal release (inclusive). "
                    "e.g. 'R37', 'R36'. Documents up to and including "
                    "this release are returned."
                ),
            },
            "from_release": {
                "type": "string",
                "description": (
                    "Start of the seasonal release range (exclusive). "
                    "Required in browse mode (no account_name). "
                    "Ignored in customer mode -- each package's installed "
                    "release from licenses.json is used instead."
                ),
            },
            "account_name": {
                "type": "string",
                "description": (
                    "Optional customer account name. When provided, "
                    "reads licenses.json to determine per-package "
                    "installed releases and filters to only the "
                    "customer's packages. Output goes under "
                    "customers/<slug>/release-docs/."
                ),
            },
            "packages": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Optional list of specific package names to include "
                    "(PE display names or LMA names). If omitted, all "
                    "packages (or all customer packages) are included."
                ),
            },
        },
        "required": ["working_directory", "to_release"],
    },
}


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 _safe_dirname(name: str) -> str:
    """Convert a package or release name to a safe directory name."""
    safe = re.sub(r"[^\w\s&()\-']", "", name)
    safe = re.sub(r"\s+", "_", safe.strip())
    return safe or "unknown"


def _resolve_release_position(
    name: str, seasonal_releases: list[dict[str, Any]],
) -> int | None:
    """Find the sr_position for a seasonal release by name (case-insensitive)."""
    for sr in seasonal_releases:
        if sr.get("name", "").lower() == name.lower():
            return sr.get("position")
    return None


def _build_sr_id_lookup(
    seasonal_releases: list[dict[str, Any]],
) -> dict[str, dict[str, Any]]:
    """Build a mapping from Seasonal_Release__c record Id to {name, position}."""
    lookup: dict[str, dict[str, Any]] = {}
    for sr in seasonal_releases:
        sr_id = sr.get("id")
        if sr_id:
            lookup[sr_id] = {"name": sr["name"], "position": sr["position"]}
    return lookup


def _build_pe_id_lookup(
    packages: dict[str, dict[str, Any]],
) -> dict[str, str]:
    """Build a mapping from PE Package__c record Id to namespace."""
    lookup: dict[str, str] = {}
    for ns, pkg_info in packages.items():
        pe_id = pkg_info.get("pe_id")
        if pe_id:
            lookup[pe_id] = ns
    return lookup


def _load_customer_package_ranges(
    working_dir: Path,
    slug: str,
    packages_info: dict[str, dict[str, Any]],
    seasonal_releases: list[dict[str, Any]],
) -> dict[str, int] | None:
    """Read licenses.json and build per-pe_id from-positions.

    Returns {pe_id: installed_sr_position} for each customer package,
    or None if licenses.json doesn't exist.
    """
    path = working_dir / "customers" / slug / "licenses.json"
    if not path.exists():
        return None

    try:
        data = json.loads(path.read_text())
    except (json.JSONDecodeError, OSError):
        return None

    pe_name_to_id: dict[str, str] = {}
    lma_name_to_id: dict[str, str] = {}
    for ns, pkg_info in packages_info.items():
        pe_id = pkg_info.get("pe_id")
        if not pe_id:
            continue
        pe_name_to_id[pkg_info.get("pe_name", "").lower()] = pe_id
        lma_name_to_id[pkg_info.get("lma_name", "").lower()] = pe_id

    ranges: dict[str, int] = {}

    vcs = data.get("version_currency_summary")
    if vcs:
        for entry in vcs:
            pkg_name = entry.get("package", "")
            lma_name = entry.get("lma_name", "")
            installed_sr = entry.get("installed_seasonal_release", "")

            pe_id = (pe_name_to_id.get(pkg_name.lower())
                     or lma_name_to_id.get(lma_name.lower())
                     or pe_name_to_id.get(lma_name.lower())
                     or lma_name_to_id.get(pkg_name.lower()))

            if not pe_id or not installed_sr or installed_sr == "Unknown":
                continue

            pos = _resolve_release_position(installed_sr, seasonal_releases)
            if pos is not None:
                ranges[pe_id] = pos
        return ranges

    for lic in data.get("production_licenses", []):
        pkg_name = lic.get("package", "")
        pe_id = (pe_name_to_id.get(pkg_name.lower())
                 or lma_name_to_id.get(pkg_name.lower()))
        if pe_id and pe_id not in ranges:
            ranges[pe_id] = -1
    return ranges


def _query_all_doc_associations(lma_alias: str) -> list[dict[str, Any]]:
    """Fetch all Document_Reference_Association__c records linked to a Package_Version."""
    query = (
        "SELECT "
        "Package_Version__r.Package__c, "
        "Package_Version__r.Seasonal_Release__c, "
        "Package_Version__r.Package_Version_No__c, "
        "Document_Reference__r.Document_Name__c, "
        "Document_Reference__r.Document_Type__c, "
        "Document_Reference__r.Document_URL__c "
        "FROM Document_Reference_Association__c "
        "WHERE Package_Version__c != null"
    )
    return sf_cli.lma_query(query, lma_alias)


def _filter_and_organise(
    records: list[dict[str, Any]],
    sr_id_lookup: dict[str, dict[str, Any]],
    pe_id_lookup: dict[str, str],
    packages_info: dict[str, dict[str, Any]],
    to_pos: int,
    to_release_name: str,
    global_from_pos: int | None,
    per_pkg_from_pos: dict[str, int] | None,
    allowed_pe_ids: set[str] | None,
) -> dict[str, dict[str, list[dict[str, Any]]]]:
    """Filter SOQL results and organise by package -> release -> docs.

    In browse mode: global_from_pos is set, per_pkg_from_pos is None.
    In customer mode: per_pkg_from_pos is set (pe_id -> installed_sr_position),
                      global_from_pos is None.

    Structure: {pe_name: {release_name: [doc_info, ...]}}
    """
    result: dict[str, dict[str, list[dict[str, Any]]]] = defaultdict(
        lambda: defaultdict(list)
    )

    for rec in records:
        pv = rec.get("Package_Version__r") or {}
        dr = rec.get("Document_Reference__r") or {}

        pkg_pe_id = pv.get("Package__c")
        sr_id = pv.get("Seasonal_Release__c")
        version_no = pv.get("Package_Version_No__c", "")
        doc_name = dr.get("Document_Name__c", "")
        doc_type = dr.get("Document_Type__c", "")
        doc_url = dr.get("Document_URL__c", "")

        if not pkg_pe_id or not sr_id or not doc_url:
            continue

        if allowed_pe_ids is not None and pkg_pe_id not in allowed_pe_ids:
            continue

        sr_info = sr_id_lookup.get(sr_id)
        if not sr_info:
            continue
        sr_pos = sr_info["position"]
        sr_name = sr_info["name"]

        if sr_pos > to_pos:
            continue

        if per_pkg_from_pos is not None:
            pkg_from = per_pkg_from_pos.get(pkg_pe_id)
            if pkg_from is None:
                continue
            if sr_pos <= pkg_from:
                continue
        elif global_from_pos is not None:
            if sr_pos <= global_from_pos:
                continue

        ns = pe_id_lookup.get(pkg_pe_id)
        if not ns:
            continue
        pe_name = packages_info.get(ns, {}).get("pe_name", ns)

        if doc_type == "User Guide" and sr_name.lower() != to_release_name.lower():
            continue

        result[pe_name][sr_name].append({
            "doc_name": doc_name,
            "doc_type": doc_type,
            "doc_url": doc_url,
            "version": version_no,
        })

    return dict(result)


def _write_folder_structure(
    base_dir: Path,
    organised: dict[str, dict[str, list[dict[str, Any]]]],
    seasonal_releases: list[dict[str, Any]],
) -> int:
    """Write the .md files for each package/release/doc. Returns file count."""
    sr_order = {sr["name"]: sr["position"] for sr in seasonal_releases}
    file_count = 0

    for pe_name, releases in sorted(organised.items()):
        pkg_dir = base_dir / _safe_dirname(pe_name)

        for sr_name in sorted(releases.keys(), key=lambda n: sr_order.get(n, 999)):
            docs = releases[sr_name]
            release_dir = pkg_dir / _safe_dirname(sr_name)
            release_dir.mkdir(parents=True, exist_ok=True)

            by_type: dict[str, list[dict[str, Any]]] = defaultdict(list)
            for doc in docs:
                by_type[doc["doc_type"]].append(doc)

            for doc_type, doc_list in by_type.items():
                filename = _safe_dirname(doc_type.lower().replace(" ", "-")) + ".md"
                filepath = release_dir / filename

                lines = [f"# {pe_name} - {sr_name} - {doc_type}\n"]
                for doc in doc_list:
                    lines.append(f"## {doc['doc_name']}\n")
                    if doc.get("version"):
                        lines.append(f"**Version:** {doc['version']}\n")
                    lines.append(f"**Link:** [{doc['doc_name']}]({doc['doc_url']})\n")
                    lines.append("")

                filepath.write_text("\n".join(lines))
                file_count += 1

    return file_count


def _write_index(
    base_dir: Path,
    organised: dict[str, dict[str, list[dict[str, Any]]]],
    seasonal_releases: list[dict[str, Any]],
    to_release: str,
    account_name: str | None,
    range_label: str,
) -> Path:
    """Write an index.md summarising all documents."""
    sr_order = {sr["name"]: sr["position"] for sr in seasonal_releases}
    base_dir.mkdir(parents=True, exist_ok=True)
    index_path = base_dir / "index.md"

    lines = ["# Release Documentation Index\n"]
    if account_name:
        lines.append(f"**Customer:** {account_name}\n")
    lines.append(f"**Range:** {range_label}\n")
    lines.append(
        f"**Generated:** "
        f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n"
    )
    lines.append("")

    total_docs = 0
    for pe_name in sorted(organised.keys()):
        releases = organised[pe_name]
        lines.append(f"## {pe_name}\n")
        pkg_dir_name = _safe_dirname(pe_name)

        for sr_name in sorted(releases.keys(), key=lambda n: sr_order.get(n, 999)):
            docs = releases[sr_name]
            release_dir_name = _safe_dirname(sr_name)
            lines.append(f"### {sr_name}\n")
            for doc in docs:
                rel_path = f"{pkg_dir_name}/{release_dir_name}"
                filename = _safe_dirname(doc["doc_type"].lower().replace(" ", "-")) + ".md"
                lines.append(
                    f"- [{doc['doc_name']}]({rel_path}/{filename}) "
                    f"({doc['doc_type']})"
                )
                total_docs += 1
            lines.append("")

    lines.insert(4, f"**Total documents:** {total_docs}\n")

    index_path.write_text("\n".join(lines))
    return index_path


def _build_ai_summary(
    organised: dict[str, dict[str, list[dict[str, Any]]]],
    seasonal_releases: list[dict[str, Any]],
    to_release: str,
    base_dir: Path,
    account_name: str | None,
    range_label: str,
) -> str:
    """Build the structured summary returned to the AI."""
    sr_order = {sr["name"]: sr["position"] for sr in seasonal_releases}

    pkg_summaries = []
    total_docs = 0
    total_release_notes = 0
    total_user_guides = 0

    for pe_name in sorted(organised.keys()):
        releases = organised[pe_name]
        pkg_releases = sorted(releases.keys(), key=lambda n: sr_order.get(n, 999))
        doc_count = sum(len(docs) for docs in releases.values())
        rn_count = sum(
            1 for docs in releases.values()
            for d in docs if d["doc_type"] == "Release Notes"
        )
        ug_count = sum(
            1 for docs in releases.values()
            for d in docs if d["doc_type"] == "User Guide"
        )
        total_docs += doc_count
        total_release_notes += rn_count
        total_user_guides += ug_count

        pkg_summaries.append(
            f"  {pe_name}: {doc_count} docs "
            f"({rn_count} release notes, {ug_count} user guides) "
            f"across {', '.join(pkg_releases)}"
        )

    label = f" for {account_name}" if account_name else ""
    header = [
        f"STATUS: READY\n",
        f"## Release Documentation{label}\n",
        f"Range: {range_label}",
        f"Packages with docs: {len(organised)}",
        f"Total documents: {total_docs} "
        f"({total_release_notes} release notes, {total_user_guides} user guides)",
        "",
        "### Per-package breakdown:\n",
    ]

    footer = [
        "",
        f"---",
        f"Output directory: {base_dir}",
        f"Index file: {base_dir / 'index.md'}",
        "",
        "Present the document list to the user as a table. Offer to:",
        "- Download the actual documents: \"Would you like me to download "
        "these documents?\" If yes, call download_release_docs. Batch by "
        "package to stay within timeout (~20-30 docs per call).",
        "- Summarise what changed between releases (based on doc titles)",
        "- Focus on a specific package's release notes",
    ]

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


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

    from_release = arguments.get("from_release", "").strip() or None
    to_release = arguments.get("to_release", "").strip()
    account_name = arguments.get("account_name", "").strip() or None
    filter_packages = arguments.get("packages") or []

    if not to_release:
        return (
            "STATUS: INVALID_RELEASE\n\n"
            "to_release is required. Example: to_release='R37'"
        )

    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"]

    seasonal_releases = cached_data.get("seasonal_releases", [])
    packages_info = cached_data.get("packages", {})

    to_pos = _resolve_release_position(to_release, seasonal_releases)
    available_names = sorted(sr.get("name", "") for sr in seasonal_releases)
    if to_pos is None:
        return (
            f"STATUS: INVALID_RELEASE\n\n"
            f"to_release '{to_release}' not found in PE data.\n\n"
            f"Available releases: {', '.join(available_names)}"
        )

    sr_id_lookup = _build_sr_id_lookup(seasonal_releases)
    if not sr_id_lookup and seasonal_releases:
        return (
            "STATUS: CACHE_REQUIRED\n\n"
            "The PE cache is missing Seasonal Release record IDs "
            "(built before v0.17.0). Call load_compass_data to "
            "refresh the cache, then retry."
        )
    pe_id_lookup = _build_pe_id_lookup(packages_info)

    global_from_pos: int | None = None
    per_pkg_from_pos: dict[str, int] | None = None
    allowed_pe_ids: set[str] | None = None
    range_label: str

    if account_name:
        slug = _make_slug(account_name)
        pkg_ranges = _load_customer_package_ranges(
            working_dir, slug, packages_info, seasonal_releases,
        )
        if pkg_ranges 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, then retry."
            )
        if not pkg_ranges:
            return (
                f"STATUS: NO_PACKAGES\n\n"
                f"licenses.json for '{account_name}' contains no packages "
                f"with resolved version currency data.\n\n"
                f"Run get_customer_licenses again to refresh."
            )

        per_pkg_from_pos = pkg_ranges
        allowed_pe_ids = set(pkg_ranges.keys())
        range_label = f"per-package installed → {to_release}"
    else:
        if not from_release:
            return (
                "STATUS: INVALID_RELEASE\n\n"
                "from_release is required in browse mode (no account_name).\n"
                "Example: from_release='R33', to_release='R37'\n\n"
                "In customer mode (with account_name), from_release is not "
                "needed -- each package's installed release is used instead."
            )
        from_pos = _resolve_release_position(from_release, seasonal_releases)
        if from_pos is None:
            return (
                f"STATUS: INVALID_RELEASE\n\n"
                f"from_release '{from_release}' not found in PE data.\n\n"
                f"Available releases: {', '.join(available_names)}"
            )
        if from_pos >= to_pos:
            return (
                f"STATUS: INVALID_RELEASE\n\n"
                f"from_release '{from_release}' (position {from_pos}) must be "
                f"before to_release '{to_release}' (position {to_pos})."
            )
        global_from_pos = from_pos
        range_label = f"{from_release} → {to_release}"

    if filter_packages:
        filter_lower = {n.lower() for n in filter_packages}
        filtered_pe_ids: set[str] = set()
        for ns, pkg_info in packages_info.items():
            pe_id = pkg_info.get("pe_id")
            if not pe_id:
                continue
            pe_name = pkg_info.get("pe_name", "")
            lma_name = pkg_info.get("lma_name", "")
            if pe_name.lower() in filter_lower or lma_name.lower() in filter_lower:
                filtered_pe_ids.add(pe_id)
        if allowed_pe_ids is not None:
            allowed_pe_ids = allowed_pe_ids & filtered_pe_ids
        else:
            allowed_pe_ids = filtered_pe_ids

        if not allowed_pe_ids:
            return (
                f"STATUS: PACKAGE_NOT_FOUND\n\n"
                f"None of the specified packages "
                f"({', '.join(filter_packages)}) were found "
                f"in the PE cache"
                + (f" or in {account_name}'s licenses" if account_name else "")
                + "."
            )

    current_state = state.load()
    lma_alias = current_state.get("lma_org_alias")
    if not lma_alias:
        return (
            "STATUS: NOT_CONNECTED\n\n"
            "No LMA org connection found. Call connect_lma first."
        )

    try:
        records = _query_all_doc_associations(lma_alias)
    except sf_cli.SfCliError as e:
        return (
            f"STATUS: QUERY_FAILED\n\n"
            f"Document query failed: {e}\n\n"
            f"The LMA session may have expired. Try calling connect_lma again."
        )

    organised = _filter_and_organise(
        records=records,
        sr_id_lookup=sr_id_lookup,
        pe_id_lookup=pe_id_lookup,
        packages_info=packages_info,
        to_pos=to_pos,
        to_release_name=to_release,
        global_from_pos=global_from_pos,
        per_pkg_from_pos=per_pkg_from_pos,
        allowed_pe_ids=allowed_pe_ids,
    )

    if not organised:
        return (
            f"STATUS: NO_DOCS_FOUND\n\n"
            f"No document references found for the range "
            f"{range_label}"
            + (f" (customer: {account_name})" if account_name else "")
            + (f" (packages: {', '.join(filter_packages)})" if filter_packages else "")
            + ".\n\n"
            f"This could mean:\n"
            f"- No documents have been uploaded to PE for this range\n"
            f"- The specified packages have no documents in PE\n"
            f"- The release range is too narrow"
        )

    if account_name:
        base_dir = working_dir / "customers" / _make_slug(account_name) / "release-docs"
    else:
        base_dir = working_dir / "release-docs"

    _write_folder_structure(base_dir, organised, seasonal_releases)
    _write_index(
        base_dir, organised, seasonal_releases,
        to_release, account_name, range_label,
    )

    return _build_ai_summary(
        organised, seasonal_releases,
        to_release, base_dir, account_name, range_label,
    )
