import shutil
from pathlib import Path

import click

from strawhub.client import StrawHubClient
from strawhub.display import print_success, print_error, console
from strawhub.errors import NotFoundError, StrawHubError, DependencyError
from strawhub.lockfile import Lockfile, PackageRef
from strawhub.paths import (
    get_root,
    get_local_root,
    get_global_root,
    get_lockfile_path,
    get_package_dir,
    get_project_file_path,
    find_installed_versions,
    package_exists,
    get_installed_version,
)
from strawhub.project_file import ProjectFile
from strawhub.tools import run_package_install, run_tool_installs_for_package
from strawhub.version_spec import (
    DependencySpec,
    extract_slug,
    parse_constraint,
    parse_version,
    compare_versions,
    satisfies_version,
)


@click.group(invoke_without_command=True)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.pass_context
def install(ctx, skip_tools, yes):
    """Install a skill, role, agent, memory, or integration with all dependencies.

    When called without a subcommand, installs all dependencies from
    strawpot.toml in the current directory.
    """
    if ctx.invoked_subcommand is None:
        _install_from_project_file(skip_tools=skip_tools, yes=yes)


def _install_impl(
    slug,
    kind,
    is_global,
    skip_tools=False,
    yes=False,
    update=False,
    recursive=False,
    version=None,
    force=False,
    save=False,
    save_exact=False,
):
    from strawhub.paths import _local_root_override

    if is_global and _local_root_override is not None:
        print_error("--root and --global cannot be used together")
        raise SystemExit(1)

    root = get_root(is_global)
    lockfile = Lockfile.load(get_lockfile_path(root))

    with StrawHubClient() as client:
        try:
            # Determine target version
            if version:
                target_version = version
                _, detail = client.get_info(slug, kind=kind, version=version)
                lv = detail.get("latestVersion")
                if not lv:
                    print_error(f"'{slug}' has no published versions.")
                    raise SystemExit(1)
                # Validate server returned the requested version
                if lv["version"] != version:
                    print_error(
                        f"Version {version} of '{slug}' is not available. "
                        f"Latest version is {lv['version']}."
                    )
                    raise SystemExit(1)
            else:
                _, detail = client.get_info(slug, kind=kind)
                lv = detail.get("latestVersion")
                if not lv:
                    print_error(f"'{slug}' has no published versions.")
                    raise SystemExit(1)
                target_version = lv["version"]

            root_ref = PackageRef(kind=kind, slug=slug, version=target_version)

            # Check existing installation
            existing_in_scope = _slug_installed_in_scope(root, kind, slug)

            if version:
                # --version: install specific version
                if existing_in_scope:
                    if not force:
                        print_error(
                            f"'{slug}' v{existing_in_scope} is already installed. "
                            "Use --force to replace."
                        )
                        raise SystemExit(1)
                    _remove_from_scope(root, lockfile, kind, slug)
            elif update:
                # --update: update to latest
                if existing_in_scope:
                    cmp = compare_versions(
                        parse_version(target_version),
                        parse_version(existing_in_scope),
                    )
                    if cmp <= 0:
                        console.print(
                            f"'{slug}' v{existing_in_scope} is already up to date."
                        )
                        # Ensure registered as direct install
                        existing_ref = PackageRef(
                            kind=kind, slug=slug, version=existing_in_scope
                        )
                        if not lockfile.is_direct_install(existing_ref.key):
                            lockfile.add_package(existing_ref)
                            lockfile.add_direct_install(existing_ref)
                            lockfile.save()
                        return
                    _remove_from_scope(root, lockfile, kind, slug)
                # If not installed, fall through to normal install
            else:
                # Normal install: skip if already installed in the target scope
                if existing_in_scope:
                    console.print(
                        f"'{slug}' v{existing_in_scope} is already installed."
                    )
                    existing_ref = PackageRef(
                        kind=kind, slug=slug, version=existing_in_scope
                    )
                    if not lockfile.is_direct_install(existing_ref.key):
                        lockfile.add_package(existing_ref)
                        lockfile.add_direct_install(existing_ref)
                        lockfile.save()
                    return

            # Resolve and install dependencies
            dep_list = _resolve_deps(client, kind, slug, detail)
            installed_deps = []

            for dep in dep_list:
                dep_slug = dep["slug"]
                dep_kind = dep["kind"]
                dep_version = dep["version"]  # latest from registry

                existing_dep_in_scope = _slug_installed_in_scope(
                    root, dep_kind, dep_slug
                )

                if update and recursive and existing_dep_in_scope:
                    # --update --recursive: update dep in target scope if outdated
                    cmp = compare_versions(
                        parse_version(dep_version),
                        parse_version(existing_dep_in_scope),
                    )
                    if cmp > 0:
                        _remove_from_scope(root, lockfile, dep_kind, dep_slug)
                        _download_package(
                            client, dep_kind, dep_slug, dep_version, root
                        )
                        dep_ref = PackageRef(
                            kind=dep_kind, slug=dep_slug, version=dep_version
                        )
                        lockfile.add_package(dep_ref, dependent=root_ref)
                        installed_deps.append(dep)
                        console.print(
                            f"  Updated {dep_kind} '{dep_slug}' "
                            f"v{existing_dep_in_scope} -> v{dep_version}"
                        )
                    else:
                        dep_ref = PackageRef(
                            kind=dep_kind,
                            slug=dep_slug,
                            version=existing_dep_in_scope,
                        )
                        if package_exists(root, dep_kind, dep_slug):
                            lockfile.add_package(dep_ref, dependent=root_ref)
                        installed_deps.append(
                            {
                                "kind": dep_kind,
                                "slug": dep_slug,
                                "version": existing_dep_in_scope,
                            }
                        )
                        console.print(
                            f"  Skipped {dep_kind} '{dep_slug}' "
                            f"v{existing_dep_in_scope} (up to date)"
                        )
                elif existing_dep_in_scope:
                    # Skip: already installed in target scope
                    dep_ref = PackageRef(
                        kind=dep_kind, slug=dep_slug, version=existing_dep_in_scope
                    )
                    lockfile.add_package(dep_ref, dependent=root_ref)
                    installed_deps.append(
                        {"kind": dep_kind, "slug": dep_slug, "version": existing_dep_in_scope}
                    )
                    console.print(
                        f"  Skipped {dep_kind} '{dep_slug}' v{existing_dep_in_scope}"
                        " (already installed)"
                    )
                else:
                    # Not installed anywhere: download
                    _download_package(
                        client, dep_kind, dep_slug, dep_version, root
                    )
                    dep_ref = PackageRef(
                        kind=dep_kind, slug=dep_slug, version=dep_version
                    )
                    lockfile.add_package(dep_ref, dependent=root_ref)
                    installed_deps.append(dep)
                    console.print(
                        f"  Installed {dep_kind} '{dep_slug}' v{dep_version}"
                    )

            # Install the root package
            _download_package(
                client, kind, slug, target_version, root, version=version
            )
            lockfile.add_package(root_ref)
            lockfile.add_direct_install(root_ref)

            # Clean up orphans (from removed old versions)
            orphans = lockfile.collect_orphans()
            for key in orphans:
                pkg = lockfile.packages.get(key)
                if not pkg:
                    continue
                pkg_dir = get_package_dir(root, pkg["kind"], pkg["slug"])
                if pkg_dir.is_dir():
                    shutil.rmtree(pkg_dir)
                lockfile.remove_package(key)

            lockfile.save()
            print_success(f"Installed {kind} '{slug}' v{target_version}")

            # Save to project file if requested
            if save or save_exact:
                pf = ProjectFile.load(get_project_file_path())
                pf.add_dependency(kind, slug, target_version, exact=save_exact)
                pf.save()
                constraint = f"=={target_version}" if save_exact else "*"
                console.print(
                    f"Saved {kind} '{slug}' ({constraint}) to strawpot.toml"
                )

            # Run package install scripts (non-fatal)
            if not skip_tools:
                for dep in installed_deps:
                    run_package_install(
                        root, dep["kind"], dep["slug"], yes=yes
                    )
                run_package_install(root, kind, slug, yes=yes)

            # Run tool installs (non-fatal)
            if not skip_tools:
                seen: set[str] = set()
                all_results: list[dict] = []
                for dep in installed_deps:
                    results = run_tool_installs_for_package(
                        root,
                        dep["kind"],
                        dep["slug"],
                        dep["version"],
                        yes=yes,
                        seen=seen,
                    )
                    all_results.extend(results)
                results = run_tool_installs_for_package(
                    root, kind, slug, target_version, yes=yes, seen=seen
                )
                all_results.extend(results)
                failed = [r for r in all_results if r["status"] == "failed"]
                if failed:
                    names = ", ".join(r["tool"] for r in failed)
                    console.print(
                        f"\n[yellow]Note:[/yellow] Some tools failed to install: "
                        f"{names}. Run 'strawhub install-tools' to retry."
                    )

        except NotFoundError:
            print_error(f"'{slug}' not found.")
            raise SystemExit(1)
        except (StrawHubError, DependencyError) as e:
            print_error(str(e))
            raise SystemExit(1)


@install.command("skill")
@click.argument("slug")
@click.option(
    "--global",
    "is_global",
    is_flag=True,
    default=False,
    help="Install to global directory (~/.strawpot or STRAWPOT_HOME)",
)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.option(
    "--update",
    is_flag=True,
    default=False,
    help="Update to the latest version if already installed",
)
@click.option(
    "--recursive",
    is_flag=True,
    default=False,
    help="With --update, also update all dependencies to latest",
)
@click.option(
    "--version",
    "ver",
    default=None,
    help="Install a specific version",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="With --version, force replace an existing installation",
)
@click.option(
    "--save",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ^X.Y.Z constraint",
)
@click.option(
    "--save-exact",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ==X.Y.Z constraint",
)
def install_skill(slug, is_global, skip_tools, yes, update, recursive, ver, force, save, save_exact):
    """Install a skill with all dependencies."""
    if recursive and not update:
        print_error("--recursive requires --update")
        raise SystemExit(1)
    if force and not ver:
        print_error("--force requires --version")
        raise SystemExit(1)
    if update and ver:
        print_error("--update and --version cannot be used together")
        raise SystemExit(1)
    if save and save_exact:
        print_error("--save and --save-exact cannot be used together")
        raise SystemExit(1)
    if (save or save_exact) and is_global:
        print_error("--save/--save-exact cannot be used with --global")
        raise SystemExit(1)
    _install_impl(
        slug,
        kind="skill",
        is_global=is_global,
        skip_tools=skip_tools,
        yes=yes,
        update=update,
        recursive=recursive,
        version=ver,
        force=force,
        save=save,
        save_exact=save_exact,
    )


@install.command("role")
@click.argument("slug")
@click.option(
    "--global",
    "is_global",
    is_flag=True,
    default=False,
    help="Install to global directory (~/.strawpot or STRAWPOT_HOME)",
)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.option(
    "--update",
    is_flag=True,
    default=False,
    help="Update to the latest version if already installed",
)
@click.option(
    "--recursive",
    is_flag=True,
    default=False,
    help="With --update, also update all dependencies to latest",
)
@click.option(
    "--version",
    "ver",
    default=None,
    help="Install a specific version",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="With --version, force replace an existing installation",
)
@click.option(
    "--save",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ^X.Y.Z constraint",
)
@click.option(
    "--save-exact",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ==X.Y.Z constraint",
)
def install_role(slug, is_global, skip_tools, yes, update, recursive, ver, force, save, save_exact):
    """Install a role with all dependencies."""
    if recursive and not update:
        print_error("--recursive requires --update")
        raise SystemExit(1)
    if force and not ver:
        print_error("--force requires --version")
        raise SystemExit(1)
    if update and ver:
        print_error("--update and --version cannot be used together")
        raise SystemExit(1)
    if save and save_exact:
        print_error("--save and --save-exact cannot be used together")
        raise SystemExit(1)
    if (save or save_exact) and is_global:
        print_error("--save/--save-exact cannot be used with --global")
        raise SystemExit(1)
    _install_impl(
        slug,
        kind="role",
        is_global=is_global,
        skip_tools=skip_tools,
        yes=yes,
        update=update,
        recursive=recursive,
        version=ver,
        force=force,
        save=save,
        save_exact=save_exact,
    )


@install.command("agent")
@click.argument("slug")
@click.option(
    "--global",
    "is_global",
    is_flag=True,
    default=False,
    help="Install to global directory (~/.strawpot or STRAWPOT_HOME)",
)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.option(
    "--update",
    is_flag=True,
    default=False,
    help="Update to the latest version if already installed",
)
@click.option(
    "--version",
    "ver",
    default=None,
    help="Install a specific version",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="With --version, force replace an existing installation",
)
@click.option(
    "--save",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ^X.Y.Z constraint",
)
@click.option(
    "--save-exact",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ==X.Y.Z constraint",
)
def install_agent(slug, is_global, skip_tools, yes, update, ver, force, save, save_exact):
    """Install an agent."""
    if force and not ver:
        print_error("--force requires --version")
        raise SystemExit(1)
    if update and ver:
        print_error("--update and --version cannot be used together")
        raise SystemExit(1)
    if save and save_exact:
        print_error("--save and --save-exact cannot be used together")
        raise SystemExit(1)
    if (save or save_exact) and is_global:
        print_error("--save/--save-exact cannot be used with --global")
        raise SystemExit(1)
    _install_impl(
        slug,
        kind="agent",
        is_global=is_global,
        skip_tools=skip_tools,
        yes=yes,
        update=update,
        version=ver,
        force=force,
        save=save,
        save_exact=save_exact,
    )


@install.command("memory")
@click.argument("slug")
@click.option(
    "--global",
    "is_global",
    is_flag=True,
    default=False,
    help="Install to global directory (~/.strawpot or STRAWPOT_HOME)",
)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.option(
    "--update",
    is_flag=True,
    default=False,
    help="Update to the latest version if already installed",
)
@click.option(
    "--version",
    "ver",
    default=None,
    help="Install a specific version",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="With --version, force replace an existing installation",
)
@click.option(
    "--save",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ^X.Y.Z constraint",
)
@click.option(
    "--save-exact",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ==X.Y.Z constraint",
)
def install_memory(slug, is_global, skip_tools, yes, update, ver, force, save, save_exact):
    """Install a memory."""
    if force and not ver:
        print_error("--force requires --version")
        raise SystemExit(1)
    if update and ver:
        print_error("--update and --version cannot be used together")
        raise SystemExit(1)
    if save and save_exact:
        print_error("--save and --save-exact cannot be used together")
        raise SystemExit(1)
    if (save or save_exact) and is_global:
        print_error("--save/--save-exact cannot be used with --global")
        raise SystemExit(1)
    _install_impl(
        slug,
        kind="memory",
        is_global=is_global,
        skip_tools=skip_tools,
        yes=yes,
        update=update,
        version=ver,
        force=force,
        save=save,
        save_exact=save_exact,
    )


@install.command("integration")
@click.argument("slug")
@click.option(
    "--global",
    "is_global",
    is_flag=True,
    default=False,
    help="Install to global directory (~/.strawpot or STRAWPOT_HOME)",
)
@click.option(
    "--skip-tools",
    is_flag=True,
    default=False,
    help="Skip running system tool install commands",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    default=False,
    help="Automatically confirm tool install commands without prompting",
)
@click.option(
    "--update",
    is_flag=True,
    default=False,
    help="Update to the latest version if already installed",
)
@click.option(
    "--version",
    "ver",
    default=None,
    help="Install a specific version",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="With --version, force replace an existing installation",
)
@click.option(
    "--save",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ^X.Y.Z constraint",
)
@click.option(
    "--save-exact",
    is_flag=True,
    default=False,
    help="Save dependency to strawpot.toml with ==X.Y.Z constraint",
)
def install_integration(slug, is_global, skip_tools, yes, update, ver, force, save, save_exact):
    """Install an integration."""
    if force and not ver:
        print_error("--force requires --version")
        raise SystemExit(1)
    if update and ver:
        print_error("--update and --version cannot be used together")
        raise SystemExit(1)
    if save and save_exact:
        print_error("--save and --save-exact cannot be used together")
        raise SystemExit(1)
    if (save or save_exact) and is_global:
        print_error("--save/--save-exact cannot be used with --global")
        raise SystemExit(1)
    _install_impl(
        slug,
        kind="integration",
        is_global=is_global,
        skip_tools=skip_tools,
        yes=yes,
        update=update,
        version=ver,
        force=force,
        save=save,
        save_exact=save_exact,
    )


# ── Dependency resolution ─────────────────────────────────────────────────────


def _resolve_deps(
    client: StrawHubClient, kind: str, slug: str, detail: dict
) -> list[dict]:
    """Resolve all transitive dependencies for a package.

    Uses the server-side /resolve endpoint for both skills and roles.
    Dependencies are just slugs; always resolves to latest version.
    """
    if kind in ("agent", "memory", "integration"):
        # Agents, memories, and integrations are standalone — no transitive dependencies
        return []
    elif kind == "role":
        console.print("Resolving dependencies...")
        resolved = client.resolve_role_deps(slug)
        deps = resolved.get("dependencies", [])
        # Filter "*" wildcard — not a real package slug
        return [d for d in deps if d.get("slug") != "*"]
    else:
        console.print("Resolving dependencies...")
        resolved = client.resolve_skill_deps(slug)
        deps = resolved.get("dependencies", [])
        return [d for d in deps if d.get("slug") != "*"]


# ── Slug-based installation helpers ───────────────────────────────────────────


def _slug_installed_in_scope(root: Path, kind: str, slug: str) -> str | None:
    """Return the installed version of a slug in a specific scope, or None.

    Assumes one version per slug per scope. If multiple exist (legacy),
    returns the highest.
    """
    versions = find_installed_versions(root, kind, slug)
    if not versions:
        return None
    versions.sort(key=parse_version, reverse=True)
    return versions[0]




def _remove_from_scope(
    root: Path, lockfile: Lockfile, kind: str, slug: str
) -> str | None:
    """Remove the installed version of a slug from a specific scope.

    Removes from disk and lockfile. Returns the removed version or None.
    """
    version = _slug_installed_in_scope(root, kind, slug)
    if not version:
        return None
    pkg_dir = get_package_dir(root, kind, slug)
    if pkg_dir.is_dir():
        shutil.rmtree(pkg_dir)
    ref = PackageRef(kind=kind, slug=slug, version=version)
    lockfile.remove_direct_install(ref)
    lockfile.remove_package(ref.key)
    return version


# ── Download ──────────────────────────────────────────────────────────────────


def _download_package(
    client: StrawHubClient,
    kind: str,
    slug: str,
    target_version: str,
    root: Path,
    version: str | None = None,
) -> None:
    """Download a package's files into root/{kind}s/{slug}/.

    When *version* is given, it is passed to the API to request a specific
    version.  Otherwise the latest version files are fetched.
    """
    target_dir = get_package_dir(root, kind, slug)
    target_dir.mkdir(parents=True, exist_ok=True)

    # Fetch detail to get file list
    _, detail = client.get_info(slug, kind=kind, version=version)
    lv = detail.get("latestVersion", {})

    for f in lv.get("files", []):
        file_path = f["path"]
        out_file = target_dir / file_path
        out_file.parent.mkdir(parents=True, exist_ok=True)

        if kind == "agent":
            content_bytes = client.get_agent_file(slug, path=file_path, version=version)
            out_file.write_bytes(content_bytes)
        elif kind == "memory":
            content_bytes = client.get_memory_file(slug, path=file_path, version=version)
            out_file.write_bytes(content_bytes)
        elif kind == "integration":
            content_bytes = client.get_integration_file(slug, path=file_path, version=version)
            out_file.write_bytes(content_bytes)
        elif kind == "skill":
            content = client.get_skill_file(slug, path=file_path, version=version)
            out_file.write_text(content, encoding="utf-8")
        else:
            content = client.get_role_file(slug, path=file_path, version=version)
            out_file.write_text(content, encoding="utf-8")

    # Write version marker for local version lookups
    (target_dir / ".version").write_text(target_version + "\n", encoding="utf-8")

    # Track the download
    client.track_download(kind, slug, version=target_version)


# ── Project file install ─────────────────────────────────────────────────────


def _install_from_project_file(skip_tools=False, yes=False):
    """Install all dependencies listed in strawpot.toml."""
    pf_path = get_project_file_path()
    pf = ProjectFile.load_if_exists(pf_path)
    if pf is None:
        print_error(
            "No strawpot.toml found. "
            "Use 'strawhub install skill <slug>' or "
            "'strawhub install role <slug>'."
        )
        raise SystemExit(1)

    if pf.is_empty:
        console.print("No dependencies in strawpot.toml. Nothing to install.")
        return

    root = get_local_root()

    for dep_kind, dep_slug, constraint_str in pf.get_all_dependencies():
        existing = _slug_installed_in_scope(root, dep_kind, dep_slug)

        if existing:
            spec = parse_constraint(constraint_str)
            full_spec = DependencySpec(
                slug=dep_slug, operator=spec.operator, version=spec.version
            )
            if satisfies_version(existing, full_spec):
                console.print(
                    f"  {dep_kind} '{dep_slug}' v{existing} "
                    f"satisfies {constraint_str} [dim](skip)[/dim]"
                )
                continue
            console.print(
                f"  {dep_kind} '{dep_slug}' v{existing} "
                f"does not satisfy {constraint_str}, reinstalling..."
            )

        # Determine version arg for exact constraints
        spec = parse_constraint(constraint_str)
        version_arg = spec.version if spec.operator == "==" else None
        force_arg = bool(existing and version_arg)

        _install_impl(
            dep_slug,
            kind=dep_kind,
            is_global=False,
            skip_tools=skip_tools,
            yes=yes,
            version=version_arg,
            force=force_arg,
        )

    print_success("All dependencies from strawpot.toml installed.")
