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

For each mapped CloudSense package, this tool:
1. Discovers available git tags via ls-remote (no clone needed)
2. Resolves the "from" tag (matching the installed version)
3. Resolves the "to" tag (matching the target upgrade version)
4. Shallow-clones each repo twice: once at from-tag, once at to-tag
5. Stores clones in package-repos/from-version/ and to-version/

Tag resolution strategy (tries in order):
- Exact installed version match (e.g., '38.0.1')
- With 'v' prefix (e.g., 'v38.0.1')
- Release train tags (e.g., 'R37', 'R38')
- Stripped leading major (e.g., '4.38.0.1' -> '38.0.1')
- Latest tag matching the release number (e.g., '38.*')

The two clones provide the basis for git diff analysis in Phase C.
"""

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

from ..utils import state, paths
from ..utils.git_cli import (
    ls_remote_tags,
    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. Discovers tags via git ls-remote, "
        "resolves version-to-tag mapping automatically, and shallow-clones "
        "into package-repos/from-version/ and to-version/. "
        "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."
                ),
            },
            "force_reclone": {
                "type": "boolean",
                "description": (
                    "If true, deletes existing clones and re-clones. "
                    "Default false."
                ),
            },
        },
        "required": ["working_directory"],
    },
}


def _extract_release_number(version_str: str) -> str | None:
    """Extract the release train number from a version string.

    '4.38.0.1' -> '38'
    'R38' -> '38'
    '38.0.1' -> '38'
    '1.211.5' -> None (ambiguous legacy format)
    """
    version_str = version_str.strip()

    m = re.match(r"^R(\d+)", version_str, re.IGNORECASE)
    if m:
        return m.group(1)

    parts = version_str.split(".")
    if len(parts) >= 2:
        for part in parts:
            num = int(part) if part.isdigit() else None
            if num is not None and 33 <= num <= 99:
                return str(num)

    return None


def _resolve_tag(
    tags: list[str],
    version_str: str,
    release_number: str | None,
) -> tuple[str | None, str]:
    """Try to find the best matching tag for a version string.

    Returns (matched_tag, strategy_used).
    """
    if not tags:
        return None, "no tags available"

    tag_set = set(tags)

    if version_str in tag_set:
        return version_str, "exact"

    if f"v{version_str}" in tag_set:
        return f"v{version_str}", "v-prefix"

    parts = version_str.split(".")
    if len(parts) >= 3:
        stripped = ".".join(parts[1:])
        if stripped in tag_set:
            return stripped, "strip-major"
        if f"v{stripped}" in tag_set:
            return f"v{stripped}", "strip-major+v"

    if f"R{version_str}" in tag_set:
        return f"R{version_str}", "R-prefix"

    if release_number:
        release_tags = []
        for t in tags:
            clean = t.lstrip("vVrR").lstrip("-")
            if clean.startswith(f"{release_number}.") or clean == release_number:
                release_tags.append(t)

        if f"R{release_number}" in tag_set:
            release_tags.append(f"R{release_number}")

        if release_tags:
            release_tags.sort(key=_tag_sort_key, reverse=True)
            return release_tags[0], "release-train-latest"

    return None, "no match"


def _tag_sort_key(tag: str) -> list[int]:
    """Generate a sort key from a tag's numeric components.

    'v38.0.2' -> [38, 0, 2]
    'R37' -> [37]
    '38.0.1' -> [38, 0, 1]
    """
    nums = re.findall(r"\d+", tag)
    return [int(n) for n in nums] if nums else [0]


def _clone_at_tag(
    url: str,
    tag: str,
    dest: Path,
    force: bool,
    report: list[str],
) -> bool:
    """Clone a repo at a specific tag. 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(
            url + ".git" if not url.endswith(".git") else url,
            dest,
            branch=tag,
            timeout=120,
        )
        report.append(f"    Cloned `{tag}` -> `{dest.name}/`")
        return True
    except GitCliError as e:
        report.append(f"    FAILED to clone `{tag}`: {e}")
        return False


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")
    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 mapping and state ─────────────────────────────────
    current_state = state.load_state(working_dir) or {}
    target_version = current_state.get("target_version", "")
    target_release = _extract_release_number(target_version)

    repo_mapping = current_state.get("repo_mapping", {})
    mapped_packages = repo_mapping.get("packages", [])

    if not mapped_packages:
        mapping_file = working_dir / "analysis" / "package-repo-mapping.json"
        if mapping_file.exists():
            data = json.loads(mapping_file.read_text())
            mapped_packages = data.get("mapped", [])

    if not mapped_packages:
        state.mark_step_failed(
            working_dir, "clone_repos",
            "No mapped packages found -- run get_repo_mapping"
        )
        return "## FAILED: No mapped packages found.\n"

    if selected_packages:
        ns_set = set(selected_packages)
        mapped_packages = [
            p for p in mapped_packages if p["namespace"] in ns_set
        ]
        if not mapped_packages:
            return (
                f"## No matching packages\n\n"
                f"None of {selected_packages} found in the mapped packages.\n"
            )

    report.append("## Clone Package Repositories\n")
    report.append(f"- Target version: **{target_version}** (release {target_release})")
    report.append(f"- Packages to clone: **{len(mapped_packages)}**")
    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 ───────────────────────────────────
    results: list[dict[str, Any]] = []
    cloned_from = 0
    cloned_to = 0
    skipped = 0
    failed_packages: list[dict[str, str]] = []

    for pkg in mapped_packages:
        ns = pkg["namespace"]
        repo_name = pkg["repo_name"]
        url = pkg["url"]
        installed_version = pkg.get("installed_version", "")
        installed_release = _extract_release_number(installed_version)

        report.append(f"\n### {ns} ({repo_name})\n")
        report.append(f"  Installed: `{installed_version}` | Target: `{target_version}`")

        # ── 3a. Discover tags ─────────────────────────────────────
        try:
            remote_tags = ls_remote_tags(url + ".git" if not url.endswith(".git") else url)
        except GitCliError as e:
            report.append(f"  FAILED to list tags: {e}")
            failed_packages.append({
                "namespace": ns, "repo": repo_name,
                "reason": f"ls-remote failed: {e}",
            })
            results.append({
                "namespace": ns, "repo_name": repo_name,
                "status": "failed", "error": str(e),
            })
            continue

        report.append(f"  Tags found: {len(remote_tags)}")

        # ── 3b. Resolve from-tag ──────────────────────────────────
        from_tag, from_strategy = _resolve_tag(
            remote_tags, installed_version, installed_release,
        )

        if from_tag:
            report.append(f"  From tag: `{from_tag}` (strategy: {from_strategy})")
        else:
            report.append(f"  From tag: **NOT RESOLVED** ({from_strategy})")

        # ── 3c. Resolve to-tag ────────────────────────────────────
        to_tag, to_strategy = _resolve_tag(
            remote_tags, target_version, target_release,
        )

        if to_tag:
            report.append(f"  To tag: `{to_tag}` (strategy: {to_strategy})")
        else:
            report.append(f"  To tag: **NOT RESOLVED** ({to_strategy})")

        # ── 3d. Handle unresolved tags ────────────────────────────
        if not from_tag and not to_tag:
            sample = sorted(remote_tags, key=_tag_sort_key, reverse=True)[:10]
            report.append(
                f"  Available tags (latest 10): "
                f"`{'`, `'.join(sample)}`"
            )
            failed_packages.append({
                "namespace": ns, "repo": repo_name,
                "reason": "neither from nor to tag resolved",
                "installed_version": installed_version,
                "available_tags_sample": sample,
            })
            results.append({
                "namespace": ns, "repo_name": repo_name,
                "status": "unresolved",
                "available_tags_count": len(remote_tags),
                "available_tags_sample": sample,
            })
            continue

        if not from_tag:
            warnings.append(
                f"{ns}: could not resolve from-tag for version "
                f"'{installed_version}'. Skipping from-version clone."
            )

        if not to_tag:
            warnings.append(
                f"{ns}: could not resolve to-tag for version "
                f"'{target_version}'. Skipping to-version clone."
            )

        # ── 3e. Clone from-version ────────────────────────────────
        result_entry: dict[str, Any] = {
            "namespace": ns,
            "repo_name": repo_name,
            "installed_version": installed_version,
            "from_tag": from_tag,
            "from_strategy": from_strategy,
            "to_tag": to_tag,
            "to_strategy": to_strategy,
        }

        if from_tag:
            dest = from_dir / repo_name
            ok = _clone_at_tag(url, from_tag, dest, force_reclone, report)
            result_entry["from_cloned"] = ok
            if ok:
                cloned_from += 1
            else:
                failed_packages.append({
                    "namespace": ns, "repo": repo_name,
                    "reason": f"clone from-tag '{from_tag}' failed",
                })
        else:
            result_entry["from_cloned"] = False

        # ── 3f. Clone to-version ──────────────────────────────────
        if to_tag:
            dest = to_dir / repo_name
            ok = _clone_at_tag(url, to_tag, dest, force_reclone, report)
            result_entry["to_cloned"] = ok
            if ok:
                cloned_to += 1
            else:
                failed_packages.append({
                    "namespace": ns, "repo": repo_name,
                    "reason": f"clone to-tag '{to_tag}' failed",
                })
        else:
            result_entry["to_cloned"] = False

        if not from_tag or not to_tag:
            skipped += 1

        results.append(result_entry)

    # ── 4. Write 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_packages": len(mapped_packages),
        "cloned_from": cloned_from,
        "cloned_to": cloned_to,
        "skipped": skipped,
        "failed": len(failed_packages),
        "results": results,
        "failed_details": failed_packages,
    }, indent=2))

    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}** partially skipped (missing tag)")
    report.append(f"- **{len(failed_packages)}** failed")
    report.append(f"\n- Results: `{results_file}`")
    report.append(f"- From repos: `{from_dir}/`")
    report.append(f"- To repos: `{to_dir}/`")

    # ── 5. 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,
            "failed": len(failed_packages),
            "total": len(mapped_packages),
            "target_version": target_version,
            "target_release": target_release,
        },
    )

    # ── 6. 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 tags, check the available tags listed above "
            "and re-run with the correct version strings if needed."
        )

    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. Ready for diff analysis.\n\n"
            "**Next step:** call `analyze_all_packages`."
        )
    else:
        report.append("\n## STATUS: PARTIAL\n")
        report.append(
            "Not all repos could be cloned. Check the warnings and "
            "failed packages above. You may need to:\n"
            "1. Verify GitHub access to trilogy-group repos\n"
            "2. Check that the target version tags exist\n"
            "3. Manually specify the correct tag names\n"
        )

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

    return "\n".join(report)
