"""get_managed_package_objects tool -- retrieves CS managed package objects.

Wildcard CustomObject retrieval (package.xml with *) does NOT include
managed package objects. This tool:
1. Reads known CS namespaces from the state file (set by get_installed_packages)
2. Discovers all sObjects in the org via sf sobject list
3. Filters to objects whose namespace matches installed CS packages
4. Keeps only real objects: __c (custom objects/settings) and __mdt
5. Excludes noise: __ChangeEvent, __History, __Share, __Feed, __e
6. Generates explicit package.xml listing each object by name
7. Retrieves in adaptive batches (progressive reduction on failure)
8. This is CRITICAL for finding customer-added fields on CS objects

No hardcoded namespace prefixes -- uses the actual installed package
namespaces from the previous step.
"""

import json
import logging
from pathlib import Path
from typing import Any
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom.minidom import parseString

from ..utils import sf_cli, state, paths

logger = logging.getLogger(__name__)

API_VERSION = "62.0"

EXCLUDED_SUFFIXES = {
    "__ChangeEvent",
    "__History",
    "__Share",
    "__Feed",
    "__e",
}

WANTED_SUFFIXES = {"__c", "__mdt"}

TOOL_DEFINITION = {
    "name": "get_managed_objects",
    "description": (
        "Retrieve CloudSense managed package objects and their custom fields. "
        "Wildcard CustomObject retrieval does NOT include managed package objects, "
        "so this tool discovers CS-namespaced sObjects (via sf sobject list), "
        "filters to __c and __mdt only (excluding ChangeEvent, History, Share, "
        "Feed), and retrieves them in adaptive batches. "
        "CRITICAL for finding customer-added fields on CS objects. "
        "Must be called AFTER get_customer_metadata."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "working_directory": {
                "type": "string",
                "description": (
                    "Absolute path to the assessment working directory."
                ),
            },
            "org_alias": {
                "type": "string",
                "description": "Salesforce org alias to query.",
            },
            "customer_name": {
                "type": "string",
                "description": (
                    "Customer name. Used to locate the SFDX project directory."
                ),
            },
        },
        "required": ["working_directory", "org_alias", "customer_name"],
    },
}


def _get_object_namespace(obj_name: str) -> str:
    """Extract the namespace prefix from an sObject API name.

    e.g., 'csord__Order__c' -> 'csord', 'Account' -> ''
    """
    if "__" in obj_name:
        return obj_name.split("__")[0]
    return ""


def _get_cs_namespaces_from_state(working_dir: Path) -> set[str]:
    """Read CS package namespaces from the state file.

    Uses the actual installed packages data from get_installed_packages,
    so no hardcoded namespace patterns are needed.
    """
    current_state = state.load_state(working_dir)
    if not current_state:
        return set()

    packages_info = current_state.get("packages_discovered", {})
    packages = packages_info.get("packages", [])

    return {
        p["namespace"]
        for p in packages
        if p.get("is_cloudsense") and p.get("namespace")
    }


def _is_wanted_object(obj_name: str) -> bool:
    """Check if sObject is a real object we want (__c or __mdt)."""
    return any(obj_name.endswith(s) for s in WANTED_SUFFIXES)


def _is_excluded(obj_name: str) -> bool:
    """Check if sObject should be excluded (ChangeEvent, History, etc.)."""
    return any(obj_name.endswith(s) for s in EXCLUDED_SUFFIXES)


def _generate_objects_package_xml(
    object_names: list[str],
    output_path: Path,
    api_version: str = API_VERSION,
) -> Path:
    """Generate a package.xml with explicit CustomObject members."""
    root = Element("Package")
    root.set("xmlns", "http://soap.sforce.com/2006/04/metadata")

    types_elem = SubElement(root, "types")
    for obj in sorted(object_names):
        member = SubElement(types_elem, "members")
        member.text = obj
    name = SubElement(types_elem, "name")
    name.text = "CustomObject"

    version = SubElement(root, "version")
    version.text = api_version

    raw_xml = tostring(root, encoding="unicode")
    pretty = parseString(raw_xml).toprettyxml(indent="    ", encoding="UTF-8")

    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_bytes(pretty)
    return output_path


def _retrieve_object_batch(
    batch_name: str,
    objects: list[str],
    manifest_dir: Path,
    project_dir: Path,
    org_alias: str,
    report: list[str],
) -> dict[str, Any]:
    """Retrieve a batch of managed package objects.

    Uses progressive reduction: if batch fails, splits in half and retries.
    """
    result = {
        "batch": batch_name,
        "objects": objects,
        "count": len(objects),
        "status": "pending",
        "file_count": 0,
    }

    manifest_path = manifest_dir / f"package-cs-objects-{batch_name}.xml"
    _generate_objects_package_xml(objects, manifest_path)

    try:
        data = sf_cli.retrieve_metadata(
            manifest_path, org_alias, cwd=project_dir
        )
        files = data.get("result", {}).get("files", [])
        file_count = len(files) if isinstance(files, list) else 0
        result["status"] = "success"
        result["file_count"] = file_count
        report.append(
            f"  - **{batch_name}**: {len(objects)} objects -> "
            f"{file_count} files retrieved"
        )
        return result

    except sf_cli.SfCliError as e:
        if len(objects) > 5:
            report.append(
                f"  - **{batch_name}**: failed with {len(objects)} objects, "
                f"splitting in half..."
            )
            mid = len(objects) // 2
            left = _retrieve_object_batch(
                f"{batch_name}-a", objects[:mid],
                manifest_dir, project_dir, org_alias, report,
            )
            right = _retrieve_object_batch(
                f"{batch_name}-b", objects[mid:],
                manifest_dir, project_dir, org_alias, report,
            )
            result["status"] = "split"
            result["file_count"] = left["file_count"] + right["file_count"]
            result["sub_batches"] = [left, right]
            return result

        else:
            report.append(
                f"  - **{batch_name}**: FAILED ({len(objects)} objects) -- {e}"
            )
            result["status"] = "failed"
            result["error"] = str(e)
            return result


async def run(arguments: dict[str, Any]) -> str:
    """Execute the get_managed_package_objects tool."""
    working_dir = Path(arguments["working_directory"])
    org_alias = arguments["org_alias"]
    customer = arguments["customer_name"]

    report: list[str] = []
    warnings: list[str] = []

    # ── 0. Verify prerequisites ───────────────────────────────────
    if not state.is_step_completed(working_dir, "get_metadata"):
        return (
            "## BLOCKER: get_customer_metadata not completed\n\n"
            "You must call get_customer_metadata first.\n"
        )

    # ── 1. Resume check ──────────────────────────────────────────
    if state.is_step_completed(working_dir, "get_managed_objects"):
        current_state = state.load_state(working_dir)
        info = (current_state or {}).get("managed_objects_retrieved", {})
        return (
            "## Step already completed\n\n"
            f"Managed package objects retrieval was already run. "
            f"Found {info.get('cs_objects', '?')} CS objects, "
            f"retrieved {info.get('total_files', '?')} files.\n\n"
            "Next step: call get_cs_settings or proceed to "
            "repository cloning."
        )

    state.mark_step_started(working_dir, "get_managed_objects")

    project_dir = working_dir / customer
    manifest_dir = paths.ensure_dir(project_dir / "manifest")

    # ── 2. Load CS namespaces from state (no hardcoding) ─────────
    cs_namespaces = _get_cs_namespaces_from_state(working_dir)
    if not cs_namespaces:
        state.mark_step_failed(
            working_dir, "get_managed_objects",
            "No CS namespaces found in state -- run get_installed_packages first"
        )
        return (
            "## BLOCKER: No CloudSense namespaces in state\n\n"
            "The state file has no CS package namespaces. "
            "Run get_installed_packages first.\n"
        )

    report.append("## Managed Package Object Discovery\n")
    report.append(
        f"Using **{len(cs_namespaces)}** CS namespaces from installed packages: "
        f"`{', '.join(sorted(cs_namespaces))}`\n"
    )

    # ── 3. Discover all sObjects ──────────────────────────────────
    try:
        all_sobjects = sf_cli.list_sobjects(org_alias, cwd=project_dir)
    except sf_cli.SfCliError as e:
        state.mark_step_failed(working_dir, "get_managed_objects", str(e))
        return f"## FAILED: Could not list sObjects\n\nError: {e}\n"

    report.append(f"Total sObjects in org: {len(all_sobjects)}\n")

    # ── 4. Filter to CS objects using actual namespaces ────────────
    cs_objects = [
        o for o in all_sobjects
        if _get_object_namespace(o) in cs_namespaces
        and _is_wanted_object(o)
        and not _is_excluded(o)
    ]

    excluded_count = sum(
        1 for o in all_sobjects
        if _get_object_namespace(o) in cs_namespaces
        and _is_excluded(o)
    )

    by_namespace: dict[str, list[str]] = {}
    for obj in cs_objects:
        ns = _get_object_namespace(obj)
        by_namespace.setdefault(ns, []).append(obj)

    report.append(f"### CloudSense Objects\n")
    report.append(f"- **{len(cs_objects)}** real CS objects (__c and __mdt)")
    report.append(f"- **{excluded_count}** excluded (ChangeEvent, History, Share, Feed, Platform Event)")
    report.append(f"- **{len(by_namespace)}** distinct namespaces\n")

    report.append("| Namespace | Objects |")
    report.append("|-----------|---------|")
    for ns in sorted(by_namespace):
        report.append(f"| {ns} | {len(by_namespace[ns])} |")

    if not cs_objects:
        state.mark_step_failed(
            working_dir, "get_managed_objects",
            "No CS-namespaced objects found"
        )
        return "\n".join(report) + (
            "\n\n## WARNING: No CS objects found. "
            "This org may not have CloudSense packages installed.\n"
        )

    # ── 4. Retrieve in batches ────────────────────────────────────
    report.append(f"\n### Retrieval\n")

    batch_size = 50
    batches = []
    for i in range(0, len(cs_objects), batch_size):
        chunk = cs_objects[i : i + batch_size]
        batch_num = (i // batch_size) + 1
        batches.append((f"batch{batch_num}", chunk))

    report.append(
        f"Retrieving {len(cs_objects)} objects in "
        f"{len(batches)} batches (up to {batch_size} objects each).\n"
    )

    all_results = []
    total_files = 0
    failed = []

    for batch_name, objects in batches:
        result = _retrieve_object_batch(
            batch_name, objects,
            manifest_dir, project_dir, org_alias, report,
        )
        all_results.append(result)
        total_files += result["file_count"]
        if result["status"] == "failed":
            failed.append(result)

    # ── 5. Write results ──────────────────────────────────────────
    output_dir = paths.ensure_dir(working_dir / "analysis")

    objects_list_file = output_dir / "cs-managed-objects.json"
    objects_list_file.write_text(json.dumps({
        "total": len(cs_objects),
        "excluded": excluded_count,
        "by_namespace": {ns: sorted(objs) for ns, objs in by_namespace.items()},
        "objects": sorted(cs_objects),
    }, indent=2))

    results_file = output_dir / "managed-objects-retrieval-results.json"
    results_file.write_text(json.dumps(all_results, indent=2))

    report.append(f"\n### Output Files\n")
    report.append(f"- CS objects list: `{objects_list_file}`")
    report.append(f"- Retrieval results: `{results_file}`")

    # ── 6. Update state ───────────────────────────────────────────
    if not failed:
        state.mark_step_completed(working_dir, "get_managed_objects")
        state.update_state(
            working_dir,
            managed_objects_retrieved={
                "cs_objects": len(cs_objects),
                "excluded": excluded_count,
                "namespaces": len(by_namespace),
                "total_files": total_files,
                "batches": len(all_results),
            },
        )
    else:
        state.mark_step_failed(
            working_dir, "get_managed_objects",
            f"{len(failed)} batch(es) failed"
        )

    # ── 7. Summary ────────────────────────────────────────────────
    report.append("\n---\n")
    if failed:
        report.append("## STATUS: PARTIAL\n")
        for f in failed:
            warnings.append(
                f"Batch '{f['batch']}' failed: {f.get('error', 'unknown')}"
            )
    else:
        report.append("## STATUS: READY\n")
        report.append(
            f"Retrieved {total_files} files for {len(cs_objects)} "
            f"CS managed package objects across {len(by_namespace)} namespaces.\n\n"
            "Customer-added custom fields on these CS objects are now "
            "available for impact analysis.\n\n"
            "**Next step:** proceed to repository cloning "
            "(`clone_package_repos`) or `get_cs_settings`."
        )

    if warnings:
        report.append("\n## Warnings\n")
        for w in warnings:
            report.append(f"- {w}")

    return "\n".join(report)
