"""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 datetime import datetime, timezone
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 _write_clone_info(
    dest: Path,
    *,
    namespace: str,
    package_name: str,
    repo_name: str,
    url: str,
    ref: str,
    ref_type: str,
    strategy: str,
    clone_type: str,
    installed_version: str,
    target_version: str,
) -> None:
    """Write .clone-info.json inside a cloned repo for traceability."""
    base_url = url.rstrip("/")
    info = {
        "namespace": namespace,
        "package_name": package_name,
        "repo_name": repo_name,
        "url": base_url,
        "ref": ref,
        "ref_type": ref_type,
        "ref_url": f"{base_url}/tree/{ref}",
        "strategy": strategy,
        "clone_type": clone_type,
        "installed_version": installed_version,
        "target_version": target_version,
        "cloned_at": datetime.now(timezone.utc).isoformat(),
    }
    try:
        (dest / ".clone-info.json").write_text(json.dumps(info, indent=2))
    except OSError as e:
        logger.warning("Failed to write .clone-info.json in %s: %s", dest, e)


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


def _write_folder_summary(
    folder: Path,
    clone_type: str,
    target_version: str,
    cloned_entries: list[dict[str, Any]],
    skipped_entries: list[dict[str, Any]],
    failed_entries: list[dict[str, str]],
) -> None:
    """Write SUMMARY.md inside from-version/ or to-version/ folder."""
    title = "From-Version Clones" if clone_type == "from" else "To-Version Clones"
    ref_key = "from_ref" if clone_type == "from" else "to_ref"
    lines = [
        f"# {title}\n",
        f"**Target upgrade:** {target_version}  ",
        f"**Cloned repos:** {len(cloned_entries)}  ",
        f"**Skipped:** {len(skipped_entries)}  ",
        f"**Failed:** {len(failed_entries)}  ",
        f"**Generated:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n",
    ]

    if cloned_entries:
        lines.append("## Cloned Repos\n")
        lines.append(
            "| # | Namespace | Repo | Ref | "
            "[Ref Link] | Usage | Tier | Complexity |"
        )
        lines.append(
            "|---|-----------|------|-----|"
            "-----------|-------|------|------------|"
        )
        for i, e in enumerate(cloned_entries, 1):
            ref = e.get(ref_key, "?")
            url = e.get("url", "").rstrip("/")
            ref_link = f"[{ref}]({url}/tree/{ref})" if url else ref
            lines.append(
                f"| {i} | {e.get('namespace', '')} "
                f"| {e.get('repo_name', '')} "
                f"| {ref} "
                f"| {ref_link} "
                f"| {e.get('customer_usage', '')} "
                f"| {e.get('usage_tier', '')} "
                f"| {e.get('migration_complexity', '')} |"
            )

    if skipped_entries:
        lines.append("\n## Skipped\n")
        lines.append("| Namespace | Repo | Reason |")
        lines.append("|-----------|------|--------|")
        for e in skipped_entries:
            lines.append(
                f"| {e.get('namespace', '')} "
                f"| {e.get('repo_name', '')} "
                f"| {e.get('skip_reason', 'unknown')} |"
            )

    if failed_entries:
        lines.append("\n## Failed\n")
        lines.append("| Namespace | Repo | Reason |")
        lines.append("|-----------|------|--------|")
        for f in failed_entries:
            lines.append(
                f"| {f.get('namespace', '')} "
                f"| {f.get('repo', '')} "
                f"| {f.get('reason', '')} |"
            )

    try:
        (folder / "SUMMARY.md").write_text("\n".join(lines))
    except OSError as e:
        logger.warning("Failed to write SUMMARY.md in %s: %s", folder, e)


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]] = []
    from_cloned_entries: list[dict[str, Any]] = []
    to_cloned_entries: list[dict[str, Any]] = []
    skipped_entries: 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
            skipped_entries.append(entry)
            continue

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

        if skip_reason == "no_code":
            report.append("  SKIPPED: no code in repository")
            skipped_count += 1
            skipped_entries.append(entry)
            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
                from_cloned_entries.append(entry)
                _write_clone_info(
                    dest,
                    namespace=ns,
                    package_name=entry.get("package_name", ""),
                    repo_name=repo_name,
                    url=url,
                    ref=from_ref,
                    ref_type=entry.get("from_ref_type", "branch"),
                    strategy=from_source,
                    clone_type="from-version",
                    installed_version=installed_version,
                    target_version=target_version,
                )
            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
                to_cloned_entries.append(entry)
                _write_clone_info(
                    dest,
                    namespace=ns,
                    package_name=entry.get("package_name", ""),
                    repo_name=repo_name,
                    url=url,
                    ref=to_ref,
                    ref_type=entry.get("to_ref_type", "branch"),
                    strategy=to_source,
                    clone_type="to-version",
                    installed_version=installed_version,
                    target_version=target_version,
                )

                # ── 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

    # ── 3g. Write folder summaries ────────────────────────────────
    _write_folder_summary(
        from_dir, "from", target_version,
        from_cloned_entries, skipped_entries, failed_packages,
    )
    _write_folder_summary(
        to_dir, "to", target_version,
        to_cloned_entries, skipped_entries, failed_packages,
    )

    # ── 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)
