"""clone_package_repos -- Clones CS repos for from-version and to-version.

Enhanced version that reads the version-ref-map produced by get_repo_mapping,
supports manual overrides, uses branch-first resolution, skips deprecated /
identical / empty-code packages, and discovers new dependencies from
the target version's sfdx-project.json.

For each mapped CloudSense package:
1. Reads the version-ref-map for pre-resolved from/to refs
2. Applies any manual overrides (override_from_ref / override_to_ref)
3. Validates refs via ls-remote --heads/--tags if not yet verified
4. Shallow-clones each repo: from-version and to-version
5. After cloning to-version, scans sfdx-project.json for new dependencies
6. Updates the version-ref-map with verified status
"""

import json
import logging
import shutil
from pathlib import Path
from typing import Any

from ..utils import state, paths
from ..utils.git_cli import (
    ls_remote_tags,
    ls_remote_branches,
    clone_shallow,
    GitCliError,
)

logger = logging.getLogger(__name__)

TOOL_DEFINITION = {
    "name": "clone_package_repos",
    "description": (
        "Clones CloudSense package repositories for the installed (from) "
        "version and target (to) version. Reads the version-ref-map for "
        "pre-resolved refs, supports manual overrides, skips deprecated/"
        "identical packages, and discovers new dependencies. "
        "Must be called AFTER get_repo_mapping."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "working_directory": {
                "type": "string",
                "description": (
                    "Absolute path to the assessment working directory."
                ),
            },
            "packages": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Optional list of namespace prefixes to clone. "
                    "If omitted, all mapped CS packages are cloned."
                ),
            },
            "include_deprecated": {
                "type": "boolean",
                "description": (
                    "If true, also clones deprecated packages. "
                    "Default false."
                ),
            },
            "include_identical": {
                "type": "boolean",
                "description": (
                    "If true, also clones packages with identical "
                    "from/to versions. Default false."
                ),
            },
            "force_reclone": {
                "type": "boolean",
                "description": (
                    "If true, deletes existing clones and re-clones. "
                    "Default false."
                ),
            },
        },
        "required": ["working_directory"],
    },
}


def _git_url(url: str) -> str:
    """Ensure URL ends with .git for clone operations."""
    return url if url.endswith(".git") else url + ".git"


def _validate_ref_exists(
    url: str,
    ref: str,
) -> tuple[bool, str]:
    """Check if a ref exists as a branch or tag on the remote.

    Returns (exists, ref_type).
    """
    try:
        branches = ls_remote_branches(_git_url(url))
        if ref in branches:
            return True, "branch"
    except GitCliError:
        pass

    try:
        tags = ls_remote_tags(_git_url(url))
        if ref in tags:
            return True, "tag"
    except GitCliError:
        pass

    return False, "not_found"


def _clone_at_ref(
    url: str,
    ref: str,
    dest: Path,
    force: bool,
    report: list[str],
) -> bool:
    """Clone a repo at a specific ref. Returns True on success."""
    if dest.exists():
        if not force:
            report.append(f"    Already cloned at `{dest.name}/`")
            return True
        shutil.rmtree(dest)

    try:
        clone_shallow(
            _git_url(url),
            dest,
            branch=ref,
            timeout=120,
        )
        report.append(f"    Cloned `{ref}` -> `{dest.name}/`")
        return True
    except GitCliError as e:
        report.append(f"    FAILED to clone `{ref}`: {e}")
        return False


def _discover_new_dependencies(
    to_clone_dir: Path,
    installed_namespaces: set[str],
    report: list[str],
) -> list[dict[str, str]]:
    """Scan sfdx-project.json in a cloned to-version repo for new deps."""
    sfdx_file = to_clone_dir / "sfdx-project.json"
    if not sfdx_file.exists():
        return []

    try:
        data = json.loads(sfdx_file.read_text())
    except (json.JSONDecodeError, OSError):
        return []

    new_deps: list[dict[str, str]] = []
    pkg_dirs = data.get("packageDirectories", [])

    for pkg_dir in pkg_dirs:
        deps = pkg_dir.get("dependencies", [])
        for dep in deps:
            dep_pkg = dep.get("package", "")
            if not dep_pkg:
                continue
            dep_ns = dep_pkg.lower().replace(" ", "")
            # Skip if already installed
            if dep_ns in installed_namespaces:
                continue
            # Skip Salesforce standard packages
            if dep_ns.startswith("0ho") or dep_ns.startswith("04t"):
                continue
            new_deps.append({
                "package_id": dep_pkg,
                "version": dep.get("versionNumber", ""),
            })

    if new_deps:
        report.append(
            f"    NEW DEPENDENCIES: {len(new_deps)} package(s) not currently installed"
        )
        for nd in new_deps:
            report.append(f"      - {nd['package_id']} ({nd['version']})")

    return new_deps


async def run(arguments: dict[str, Any]) -> str:
    """Execute the clone_package_repos tool."""
    working_dir = Path(arguments["working_directory"])
    selected_packages = arguments.get("packages")
    include_deprecated = arguments.get("include_deprecated", False)
    include_identical = arguments.get("include_identical", False)
    force_reclone = arguments.get("force_reclone", False)

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

    # ── 0. Prerequisites ─────────────────────────────────────────
    if not state.is_step_completed(working_dir, "get_repo_mapping"):
        return (
            "## BLOCKER: get_repo_mapping not completed\n\n"
            "You must call get_repo_mapping first.\n"
        )

    # ── 1. Resume check ──────────────────────────────────────────
    if (
        state.is_step_completed(working_dir, "clone_repos")
        and not force_reclone
    ):
        current_state = state.load_state(working_dir)
        info = (current_state or {}).get("repos_cloned", {})
        return (
            "## Step already completed\n\n"
            f"Repos already cloned: {info.get('cloned_from', '?')} "
            f"from-version, {info.get('cloned_to', '?')} to-version.\n"
            f"{info.get('skipped', 0)} skipped, "
            f"{info.get('failed', 0)} failed.\n\n"
            "Pass `force_reclone: true` to re-clone.\n\n"
            "**Next step:** call `analyze_all_packages`."
        )

    state.mark_step_started(working_dir, "clone_repos")

    # ── 2. Load version-ref-map ───────────────────────────────────
    ref_map_file = working_dir / "analysis" / "version-ref-map.json"
    if not ref_map_file.exists():
        state.mark_step_failed(
            working_dir, "clone_repos",
            "version-ref-map.json not found -- run get_repo_mapping"
        )
        return "## FAILED: version-ref-map.json not found.\n"

    ref_map_data = json.loads(ref_map_file.read_text())
    ref_entries = ref_map_data.get("packages", [])
    target_version = ref_map_data.get("target_version", "")
    target_release = ref_map_data.get("target_release", "")

    if not ref_entries:
        state.mark_step_failed(
            working_dir, "clone_repos",
            "No packages in version-ref-map"
        )
        return "## FAILED: No packages in version-ref-map.\n"

    # Filter by selected namespaces
    if selected_packages:
        ns_set = set(selected_packages)
        ref_entries = [e for e in ref_entries if e["namespace"] in ns_set]
        if not ref_entries:
            return (
                f"## No matching packages\n\n"
                f"None of {selected_packages} found in the version-ref-map.\n"
            )

    # Load installed namespaces for new dependency detection
    current_state = state.load_state(working_dir) or {}
    packages_info = current_state.get("packages_discovered", {})
    installed_packages = packages_info.get("packages", [])
    installed_namespaces = {
        (p.get("namespace") or "").lower()
        for p in installed_packages
        if p.get("namespace")
    }

    report.append("## Clone Package Repositories\n")
    report.append(f"- Target version: **{target_version}**")
    report.append(f"- Packages in ref-map: **{len(ref_entries)}**")
    report.append(f"- Include deprecated: {include_deprecated}")
    report.append(f"- Include identical: {include_identical}")
    report.append(f"- Force re-clone: {force_reclone}\n")

    from_dir = paths.ensure_dir(working_dir / "package-repos" / "from-version")
    to_dir = paths.ensure_dir(working_dir / "package-repos" / "to-version")

    # ── 3. Process each package ───────────────────────────────────
    cloned_from = 0
    cloned_to = 0
    skipped_count = 0
    failed_packages: list[dict[str, str]] = []
    all_new_deps: list[dict[str, Any]] = []

    for entry in ref_entries:
        ns = entry["namespace"]
        repo_name = entry["repo_name"]
        url = entry["url"]
        skip_reason = entry.get("skip_reason")

        report.append(f"\n### {ns} ({repo_name})\n")

        # ── 3a. Check skip conditions ────────────────────────────
        if skip_reason == "deprecated" and not include_deprecated:
            report.append("  SKIPPED: deprecated")
            skipped_count += 1
            continue

        if skip_reason == "identical_versions" and not include_identical:
            report.append("  SKIPPED: from/to versions are identical")
            skipped_count += 1
            continue

        if skip_reason == "no_code":
            report.append("  SKIPPED: no code in repository")
            skipped_count += 1
            continue

        # ── 3b. Determine effective refs (overrides take priority) ──
        from_ref = entry.get("override_from_ref") or entry.get("from_ref")
        to_ref = entry.get("override_to_ref") or entry.get("to_ref")

        from_source = (
            "override" if entry.get("override_from_ref")
            else entry.get("from_strategy", "auto")
        )
        to_source = (
            "override" if entry.get("override_to_ref")
            else entry.get("to_strategy", "auto")
        )

        installed_version = entry.get("installed_version", "")
        report.append(
            f"  Installed: `{installed_version}` | Target: `{target_version}`"
        )

        if from_ref:
            report.append(
                f"  From ref: `{from_ref}` (source: {from_source})"
            )
        else:
            report.append("  From ref: **NOT RESOLVED**")

        if to_ref:
            report.append(f"  To ref: `{to_ref}` (source: {to_source})")
        else:
            report.append("  To ref: **NOT RESOLVED**")

        # ── 3c. Handle unresolved refs ────────────────────────────
        if not from_ref and not to_ref:
            failed_packages.append({
                "namespace": ns,
                "repo": repo_name,
                "reason": "neither from nor to ref resolved",
            })
            continue

        if not from_ref:
            warnings.append(
                f"{ns}: from-ref not resolved for version "
                f"'{installed_version}'. Skipping from-version clone."
            )

        if not to_ref:
            warnings.append(
                f"{ns}: to-ref not resolved for version "
                f"'{target_version}'. Skipping to-version clone."
            )

        # ── 3d. Clone from-version ────────────────────────────────
        from_ok = False
        if from_ref:
            dest = from_dir / repo_name
            from_ok = _clone_at_ref(url, from_ref, dest, force_reclone, report)
            if from_ok:
                cloned_from += 1
                entry["from_verified"] = True
            else:
                failed_packages.append({
                    "namespace": ns,
                    "repo": repo_name,
                    "reason": f"clone from-ref '{from_ref}' failed",
                })

        # ── 3e. Clone to-version ──────────────────────────────────
        to_ok = False
        if to_ref:
            dest = to_dir / repo_name
            to_ok = _clone_at_ref(url, to_ref, dest, force_reclone, report)
            if to_ok:
                cloned_to += 1
                entry["to_verified"] = True

                # ── 3f. Discover new dependencies ─────────────────
                new_deps = _discover_new_dependencies(
                    dest, installed_namespaces, report,
                )
                if new_deps:
                    for nd in new_deps:
                        nd["required_by"] = ns
                    all_new_deps.extend(new_deps)
            else:
                failed_packages.append({
                    "namespace": ns,
                    "repo": repo_name,
                    "reason": f"clone to-ref '{to_ref}' failed",
                })

        if not from_ref or not to_ref:
            skipped_count += 1

    # ── 4. Write updated version-ref-map ──────────────────────────
    ref_map_file.write_text(json.dumps(ref_map_data, indent=2))

    # ── 5. Write clone results ────────────────────────────────────
    output_dir = paths.ensure_dir(working_dir / "analysis")
    results_file = output_dir / "clone-results.json"
    results_file.write_text(json.dumps({
        "target_version": target_version,
        "target_release": target_release,
        "total_in_ref_map": len(ref_entries),
        "cloned_from": cloned_from,
        "cloned_to": cloned_to,
        "skipped": skipped_count,
        "failed": len(failed_packages),
        "new_dependencies": all_new_deps,
        "failed_details": failed_packages,
    }, indent=2))

    # ── 6. Report summary ────────────────────────────────────────
    report.append(f"\n### Summary\n")
    report.append(f"- **{cloned_from}** from-version repos cloned")
    report.append(f"- **{cloned_to}** to-version repos cloned")
    report.append(f"- **{skipped_count}** skipped")
    report.append(f"- **{len(failed_packages)}** failed")
    report.append(f"\n- Clone results: `{results_file}`")
    report.append(f"- From repos: `{from_dir}/`")
    report.append(f"- To repos: `{to_dir}/`")

    # ── 7. New dependencies section ───────────────────────────────
    if all_new_deps:
        report.append(f"\n### NEW DEPENDENCIES DISCOVERED\n")
        report.append(
            f"**{len(all_new_deps)}** package(s) required by the target "
            f"version that are NOT currently installed:\n"
        )
        report.append("| Package | Version | Required By |")
        report.append("|---------|---------|-------------|")

        deps_by_pkg: dict[str, dict[str, Any]] = {}
        for nd in all_new_deps:
            pkg_id = nd["package_id"]
            if pkg_id not in deps_by_pkg:
                deps_by_pkg[pkg_id] = {
                    "version": nd.get("version", ""),
                    "required_by": [],
                }
            deps_by_pkg[pkg_id]["required_by"].append(nd["required_by"])

        for pkg_id, info in deps_by_pkg.items():
            required_by = ", ".join(info["required_by"])
            report.append(
                f"| {pkg_id} | {info['version']} | {required_by} |"
            )

        warnings.append(
            f"UPGRADE REQUIRES {len(deps_by_pkg)} NEW PACKAGE(S): "
            + ", ".join(deps_by_pkg.keys())
            + ". These must be installed BEFORE upgrading dependent packages."
        )

    # ── 8. Update state ──────────────────────────────────────────
    has_both = cloned_from > 0 and cloned_to > 0
    if has_both:
        state.mark_step_completed(working_dir, "clone_repos")
    else:
        state.mark_step_failed(
            working_dir, "clone_repos",
            f"Incomplete: {cloned_from} from, {cloned_to} to"
        )

    state.update_state(
        working_dir,
        repos_cloned={
            "cloned_from": cloned_from,
            "cloned_to": cloned_to,
            "skipped": skipped_count,
            "failed": len(failed_packages),
            "total": len(ref_entries),
            "target_version": target_version,
            "target_release": target_release,
            "new_dependencies": len(all_new_deps),
        },
    )

    # ── 9. Final report ──────────────────────────────────────────
    report.append("\n---\n")

    if failed_packages:
        report.append("## Unresolved / Failed Packages\n")
        report.append("| Namespace | Repo | Reason |")
        report.append("|-----------|------|--------|")
        for f in failed_packages:
            report.append(
                f"| {f['namespace']} | {f['repo']} | {f['reason']} |"
            )
        report.append(
            "\nFor unresolved refs, edit `version-ref-map.json` to set "
            "`override_from_ref` / `override_to_ref` and re-run."
        )

    if has_both:
        report.append("\n## STATUS: READY\n")
        report.append(
            f"Successfully cloned {cloned_from} from-version and "
            f"{cloned_to} to-version repos.\n\n"
            "**Next step:** call `analyze_all_packages`."
        )
    else:
        report.append("\n## STATUS: PARTIAL\n")
        report.append(
            "Not all repos could be cloned. Options:\n"
            "1. Edit `version-ref-map.json` to set override refs\n"
            "2. Verify GitHub access to trilogy-group repos\n"
            "3. Re-run with `force_reclone: true`\n"
        )

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

    return "\n".join(report)
