"""complyform update — check for CLI and profile bundle updates.

Two independent checks: PyPI for CLI version, backend for profile bundles.
"""

from __future__ import annotations

import hashlib
import json
import os
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Literal

import typer
from pydantic import BaseModel, ConfigDict

from complyform.cli.exit_codes import EXIT_SUCCESS
from complyform.cli.main import AppContext, app

if TYPE_CHECKING:
    from rich.console import Console

_MG = "Management"

_PYPI_URL = "https://pypi.org/pypi/complyform/json"
_PROFILES_CHECK_URL = "/v1/profiles/check"


class ProfileChangeEntry(BaseModel):
    """Single change in a profile bundle update."""

    model_config = ConfigDict(strict=True)

    change_type: Literal["added", "modified", "removed", "reclassified"]
    framework: str
    control_ids: list[str] | None = None
    description: str
    source_ref: str | None = None
    breaking: bool


class ProfileUpdateCheck(BaseModel):
    """Response from profile update check endpoint."""

    model_config = ConfigDict(strict=True)

    current_bundle_version: str
    latest_bundle_version: str
    update_available: bool
    download_url: str | None = None
    manifest_url: str | None = None
    min_cli_version: str | None = None
    changelog: str | None = None
    changelog_entries: list[ProfileChangeEntry] | None = None
    changelog_url: str | None = None


def _check_cli_version(console: Console) -> dict[str, object]:
    """Check PyPI for newer CLI version. Returns result dict."""
    from complyform import __version__

    result: dict[str, object] = {
        "current": __version__,
        "latest": __version__,
        "update_available": False,
    }

    try:
        import httpx
        from packaging.version import Version

        resp = httpx.get(_PYPI_URL, timeout=5.0)
        if resp.status_code == 200:
            remote = resp.json()["info"]["version"]
            result["latest"] = remote
            if Version(remote) > Version(__version__):
                result["update_available"] = True
                console.print(
                    f"Update available: complyform {remote}"
                    f" (you have {__version__}).\n"
                    "Run: [bold]uv tool install --upgrade complyform[/bold]"
                )
            else:
                console.print(f"complyform is up to date ({__version__})")
        else:
            console.print(
                f"[dim]Could not check for CLI updates (HTTP {resp.status_code})[/dim]"
            )
    except Exception:
        console.print(
            "[dim]Could not check for CLI updates (network unavailable)[/dim]"
        )

    return result


def _check_profile_updates(
    console: Console,
    *,
    tier: str,
    api_key: str | None,
    check_only: bool,
) -> dict[str, object]:
    """Check for profile bundle updates. Returns result dict."""
    result: dict[str, object] = {
        "current": None,
        "latest": None,
        "update_available": False,
    }

    if tier == "community":
        return result

    version_file = Path.home() / ".complyform" / "profiles" / "VERSION"
    if not version_file.exists():
        return result

    local_version = version_file.read_text(encoding="utf-8").strip()
    result["current"] = local_version

    if not api_key:
        console.print("[dim]Cannot check profile updates — no API key configured[/dim]")
        return result

    try:
        import httpx

        # Determine API base URL
        api_base = os.environ.get("COMPLYFORM_API_BASE", "https://api.complyform.dev")

        resp = httpx.post(
            f"{api_base}{_PROFILES_CHECK_URL}",
            json={"bundle_version": local_version, "tier": tier},
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=5.0,
        )

        if resp.status_code != 200:
            console.print(
                f"[dim]Profile update check failed (HTTP {resp.status_code})[/dim]"
            )
            return result

        check = ProfileUpdateCheck.model_validate(resp.json())
        result["latest"] = check.latest_bundle_version
        result["update_available"] = check.update_available

        if check.changelog_entries:
            result["changelog_entries"] = [
                e.model_dump(mode="json") for e in check.changelog_entries
            ]

        if not check.update_available:
            console.print(f"Profiles are up to date (v{local_version})")
            return result

        # Check min_cli_version
        if check.min_cli_version:
            from packaging.version import Version

            from complyform import __version__

            if Version(__version__) < Version(check.min_cli_version):
                console.print(
                    f"Profile update v{check.latest_bundle_version}"
                    f" requires CLI >= {check.min_cli_version}.\n"
                    "Update CLI first: [bold]uv tool install"
                    " --upgrade complyform[/bold]"
                )
                return result

        console.print(
            f"Profile update available: v{check.latest_bundle_version}"
            f" (you have v{local_version})"
        )

        if check_only:
            return result

        # Download and install
        if check.download_url:
            _download_and_install_profiles(console, check)

    except Exception as e:
        console.print(f"[dim]Could not check for profile updates: {e}[/dim]")

    return result


def _download_and_install_profiles(console: Console, check: ProfileUpdateCheck) -> None:
    """Download and atomically install profile bundle."""
    if not check.download_url:
        return

    import httpx

    console.print(f"Downloading profiles v{check.latest_bundle_version}...")

    resp = httpx.get(check.download_url, timeout=30.0, follow_redirects=True)
    resp.raise_for_status()

    # Verify checksum if manifest available
    if check.manifest_url:
        manifest_resp = httpx.get(
            check.manifest_url, timeout=10.0, follow_redirects=True
        )
        if manifest_resp.status_code == 200:
            manifest = manifest_resp.json()
            expected_sha = manifest.get("sha256")
            if expected_sha:
                actual_sha = hashlib.sha256(resp.content).hexdigest()
                if actual_sha != expected_sha:
                    console.print(
                        "[red]Checksum mismatch — download corrupted. Aborting.[/red]"
                    )
                    return

    # Extract to temp, then atomic replace
    profiles_dir = Path.home() / ".complyform" / "profiles"
    pid = os.getpid()
    tmp_dir = Path.home() / ".complyform" / f"profiles-tmp-{pid}"

    try:
        import io
        import tarfile

        tmp_dir.mkdir(parents=True, exist_ok=True)
        with tarfile.open(fileobj=io.BytesIO(resp.content), mode="r:gz") as tar:
            tar.extractall(path=tmp_dir, filter="data")

        # Atomic replacement
        os.replace(tmp_dir, profiles_dir)

        # Write VERSION
        version_file = profiles_dir / "VERSION"
        version_file.write_text(check.latest_bundle_version, encoding="utf-8")

        console.print(
            f"[green]\u2713 Profiles updated to v{check.latest_bundle_version}[/green]"
        )

        # Show changelog summary
        if check.changelog_entries:
            _display_changelog_entries(console, check.changelog_entries[:5])

    except Exception as e:
        console.print(f"[red]Profile install failed: {e}[/red]")
        # Clean up temp dir
        if tmp_dir.exists():
            import shutil

            shutil.rmtree(tmp_dir, ignore_errors=True)


def _display_changelog_entries(
    console: Console,
    entries: list[ProfileChangeEntry],
) -> None:
    """Display changelog entries in a Rich table."""
    from rich.table import Table

    table = Table(title="Changelog", show_lines=False)
    table.add_column("Type", style="cyan", width=12)
    table.add_column("Framework", style="green")
    table.add_column("Description")
    table.add_column("Breaking", width=8)

    for entry in entries:
        breaking = "[red]YES[/red]" if entry.breaking else ""
        table.add_row(
            entry.change_type,
            entry.framework,
            entry.description,
            breaking,
        )

    console.print(table)


def _display_local_changelog(console: Console) -> None:
    """Display local changelog — no network calls."""
    # Profile changelog
    changelog_file = Path.home() / ".complyform" / "profiles" / "profile-changelog.json"
    if changelog_file.exists():
        try:
            data = json.loads(changelog_file.read_text(encoding="utf-8"))
            entries_raw = data.get("entries", [])[:5]
            if entries_raw:
                console.print("\n[bold]Profile Changelog[/bold]")
                for entry in entries_raw:
                    ver = entry.get("version", "?")
                    date = entry.get("date", "?")
                    summary = entry.get("summary", "")
                    breaking = " [red](BREAKING)[/red]" if entry.get("breaking") else ""
                    console.print(f"  v{ver} ({date}){breaking}: {summary}")
                    changes = entry.get("changes", [])
                    for change in changes[:3]:
                        console.print(f"    - {change}")
            else:
                console.print("[dim]No profile changelog entries.[/dim]")
        except Exception:
            console.print("[dim]Could not read profile changelog.[/dim]")
    else:
        console.print("[dim]No profile changelog found (no profiles installed).[/dim]")


@app.command(rich_help_panel=_MG)
def update(
    ctx: typer.Context,
    check: Annotated[
        bool,
        typer.Option("--check", help="Check only, don't install"),
    ] = False,
    changelog: Annotated[
        bool,
        typer.Option("--changelog", help="Display recent release notes"),
    ] = False,
    format: Annotated[
        str,
        typer.Option("--format", "-f", help="Output format: terminal|json"),
    ] = "terminal",
) -> None:
    """Check for CLI and framework profile updates."""
    app_ctx: AppContext = ctx.obj
    config = app_ctx.config
    console = app_ctx.console

    if changelog:
        _display_local_changelog(console)
        raise typer.Exit(code=EXIT_SUCCESS)

    # Run both checks
    cli_result = _check_cli_version(console)

    console.print()  # Separator

    profile_result = _check_profile_updates(
        console,
        tier=config.tier,
        api_key=config.api_key,
        check_only=check,
    )

    if format == "json":
        from complyform import __version__

        envelope = {
            "schema_version": 1,
            "tool": "complyform",
            "tool_version": __version__,
            "command": "update",
            "timestamp": datetime.now(tz=UTC).isoformat(),
            "result": {
                "cli": cli_result,
                "profiles": profile_result,
            },
        }
        sys.stdout.write(json.dumps(envelope, indent=2) + "\n")

    raise typer.Exit(code=EXIT_SUCCESS)
