"""Download utilities using urllib with progress tracking."""

from __future__ import annotations

import filecmp
import hashlib
import json
import shutil
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Callable


@dataclass
class DownloadConfig:
    """Configuration for download operations."""

    repo_url: str
    repo_branch: str
    local_mode: bool = False
    local_repo_dir: Path | None = None


@dataclass
class FileInfo:
    """File information including path and optional SHA hash."""

    path: str
    sha: str | None = None


def compute_git_blob_sha(file_path: Path) -> str:
    """Compute git blob SHA1 hash for a file (same algorithm git uses)."""
    content = file_path.read_bytes()
    header = f"blob {len(content)}\0".encode()
    return hashlib.sha1(header + content).hexdigest()


def get_cache_path() -> Path:
    """Get path to the tree cache file."""
    return Path.home() / ".tribunal" / "cache" / "tree-cache.json"


def load_tree_cache(cache_path: Path | None = None) -> dict:
    """Load cached tree data from disk."""
    if cache_path is None:
        cache_path = get_cache_path()
    if not cache_path.exists():
        return {}
    try:
        return json.loads(cache_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def save_tree_cache(cache_path: Path | None, cache_data: dict) -> None:
    """Save tree cache data to disk."""
    if cache_path is None:
        cache_path = get_cache_path()
    cache_path.parent.mkdir(parents=True, exist_ok=True)
    cache_path.write_text(json.dumps(cache_data, indent=2))


def download_file(
    repo_path: str | FileInfo,
    dest_path: Path,
    config: DownloadConfig,
    progress_callback: Callable[[int, int], None] | None = None,
) -> bool:
    """Download a file from the repository or copy in local mode.

    Skips download if destination file exists and has matching content/hash.
    """
    if isinstance(repo_path, FileInfo):
        file_sha = repo_path.sha
        repo_path = repo_path.path
    else:
        file_sha = None

    dest_path.parent.mkdir(parents=True, exist_ok=True)

    if config.local_mode and config.local_repo_dir:
        source_file = config.local_repo_dir / repo_path
        # Guard against path traversal: ensure the resolved source stays
        # within local_repo_dir (a repo_path of "../../etc/passwd" would
        # otherwise escape the intended directory).
        try:
            resolved_source = source_file.resolve()
            resolved_base = config.local_repo_dir.resolve()
            resolved_source.relative_to(resolved_base)
        except ValueError:
            return False
        if source_file.is_file():
            try:
                if resolved_source == dest_path.resolve():
                    return True
                if dest_path.exists() and filecmp.cmp(source_file, dest_path, shallow=False):
                    return True
                shutil.copy2(source_file, dest_path)
                return True
            except (OSError, IOError):
                return False
        return False

    if file_sha and dest_path.exists():
        try:
            local_sha = compute_git_blob_sha(dest_path)
            if local_sha == file_sha:
                return True
        except (OSError, IOError):
            pass

    file_url = f"{config.repo_url}/raw/{config.repo_branch}/{repo_path}"
    try:
        request = urllib.request.Request(file_url)
        with urllib.request.urlopen(request, timeout=30.0) as response:
            if response.status != 200:
                return False

            total = int(response.headers.get("content-length", 0))
            downloaded = 0

            with open(dest_path, "wb") as f:
                while True:
                    chunk = response.read(8192)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    if progress_callback and total > 0:
                        progress_callback(downloaded, total)

        return True
    except (urllib.error.URLError, OSError, TimeoutError):
        return False


def download_files_parallel(
    file_infos: list[FileInfo],
    dest_paths: list[Path],
    config: DownloadConfig,
    max_workers: int = 8,
) -> list[bool]:
    """Download multiple files in parallel using ThreadPoolExecutor.

    Returns a list of booleans indicating success/failure for each file,
    in the same order as the input lists.
    """
    if len(file_infos) != len(dest_paths):
        raise ValueError("file_infos and dest_paths must have the same length")

    if not file_infos:
        return []

    results: list[bool | None] = [None] * len(file_infos)

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_index = {
            executor.submit(download_file, file_info, dest_path, config): i
            for i, (file_info, dest_path) in enumerate(zip(file_infos, dest_paths))
        }

        for future in as_completed(future_to_index):
            index = future_to_index[future]
            try:
                results[index] = future.result()
            except Exception:
                results[index] = False

    return [r if r is not None else False for r in results]


def _files_from_cache(cached_files: list[dict], dir_path: str) -> list[FileInfo]:
    """Convert cached file dicts to FileInfo objects, filtering by dir_path."""
    return [FileInfo(path=f["path"], sha=f.get("sha")) for f in cached_files if f.get("path", "").startswith(dir_path)]


def get_repo_files(dir_path: str, config: DownloadConfig) -> list[FileInfo]:
    """Get all files from a repository directory.

    Returns FileInfo objects. Remote mode includes SHA hashes for skip-if-unchanged.
    Local mode has sha=None (uses filecmp for comparison instead).
    Uses ETag caching to avoid re-fetching unchanged data from GitHub API.
    """
    if config.local_mode and config.local_repo_dir:
        source_dir = config.local_repo_dir / dir_path
        if source_dir.is_dir():
            result: list[FileInfo] = []
            for file_path in source_dir.rglob("*"):
                if file_path.is_file():
                    rel_path = file_path.relative_to(config.local_repo_dir)
                    result.append(FileInfo(path=str(rel_path), sha=None))
            return result
        return []

    cache_path = get_cache_path()
    cache = load_tree_cache(cache_path)
    branch_cache = cache.get(config.repo_branch, {})
    cached_etag = branch_cache.get("etag")
    cached_files = branch_cache.get("files", [])

    try:
        repo_path = config.repo_url.replace("https://github.com/", "")
        tree_url = f"https://api.github.com/repos/{repo_path}/git/trees/{config.repo_branch}?recursive=true"

        request = urllib.request.Request(tree_url)
        if cached_etag:
            request.add_header("If-None-Match", cached_etag)

        with urllib.request.urlopen(request, timeout=30.0) as response:
            if response.status != 200:
                return []

            new_etag = response.headers.get("ETag")
            data = json.loads(response.read().decode("utf-8"))

            all_files: list[dict] = []
            remote_files: list[FileInfo] = []
            if "tree" in data:
                for item in data["tree"]:
                    if item.get("type") == "blob":
                        path = item.get("path", "")
                        sha = item.get("sha")
                        all_files.append({"path": path, "sha": sha})
                        if path.startswith(dir_path):
                            remote_files.append(FileInfo(path=path, sha=sha))

            if new_etag and all_files:
                cache[config.repo_branch] = {"etag": new_etag, "files": all_files}
                save_tree_cache(cache_path, cache)

            return remote_files
    except urllib.error.HTTPError as e:
        if e.code == 304 and cached_files:
            return _files_from_cache(cached_files, dir_path)
        return []
    except (urllib.error.URLError, json.JSONDecodeError, TimeoutError):
        return []


# ---------------------------------------------------------------------------
# Vault SHA-256 integrity verification
# ---------------------------------------------------------------------------

def compute_sha256(file_path: Path) -> str:
    """Compute SHA-256 digest of a file."""
    h = hashlib.sha256()
    with file_path.open("rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def load_vault_manifest(manifest_path: Path) -> dict:
    """Load vault manifest.json {relative_path: sha256}."""
    if not manifest_path.exists():
        return {}
    try:
        return json.loads(manifest_path.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def verify_vault_files(dest_dir: Path, manifest: dict) -> list[str]:
    """Verify downloaded vault files against SHA-256 manifest.

    Returns list of mismatched relative paths. Empty list means all OK.
    Raises SystemExit if any mismatch is found (fail loudly).
    """
    failures: list[str] = []
    for rel_path, expected_sha in manifest.items():
        abs_path = dest_dir / rel_path
        if not abs_path.exists():
            continue  # Missing files are checked separately
        actual_sha = compute_sha256(abs_path)
        if actual_sha != expected_sha:
            failures.append(rel_path)
    return failures


def verify_vault_integrity(dest_dir: Path, manifest_path: Path) -> None:
    """Verify all vault files match manifest.json. Fail loudly on mismatch.

    Call this after downloading vault files to ensure integrity.
    Raises SystemExit with a non-zero code if any file fails SHA-256 check.
    """
    manifest = load_vault_manifest(manifest_path)
    if not manifest:
        return  # No manifest, skip verification

    failures = verify_vault_files(dest_dir, manifest)
    if failures:
        import sys as _sys
        print(
            f"\n[SECURITY] SHA-256 integrity check FAILED for {len(failures)} vault file(s):",
            file=_sys.stderr,
        )
        for f in failures:
            print(f"  MISMATCH: {f}", file=_sys.stderr)
        print(
            "[SECURITY] Vault files may be corrupt or tampered. Aborting installation.",
            file=_sys.stderr,
        )
        _sys.exit(1)


# ---------------------------------------------------------------------------
# GPG signature verification for vault manifest
# ---------------------------------------------------------------------------

def verify_manifest_gpg_signature(manifest_path: Path, audit_log_fn=None) -> bool:
    """Attempt to verify manifest.json.sig against manifest.json using GPG.

    Downloads manifest.json.sig alongside manifest.json if it exists.
    Returns True if verification passed, False if unavailable or failed.
    Warns (never fails) if gpg is not installed.
    Logs verification result via *audit_log_fn* if provided.
    """
    import shutil
    import subprocess as _subprocess

    sig_path = manifest_path.with_suffix(".json.sig")
    if not sig_path.exists():
        if audit_log_fn:
            audit_log_fn("gpg_verify", "skipped", "manifest.json.sig not present")
        return False

    if not shutil.which("gpg"):
        import sys as _sys
        print(
            "[WARN] GPG not installed — skipping manifest signature verification. "
            "Install gpg for supply-chain security.",
            file=_sys.stderr,
        )
        if audit_log_fn:
            audit_log_fn("gpg_verify", "skipped", "gpg not installed")
        return False

    try:
        result = _subprocess.run(
            ["gpg", "--batch", "--no-tty", "--verify", str(sig_path), str(manifest_path)],
            capture_output=True,
            text=True,
            timeout=15,
        )
        ok = result.returncode == 0
        status = "passed" if ok else "failed"
        detail = result.stderr.strip()
        if audit_log_fn:
            audit_log_fn("gpg_verify", status, detail)
        if not ok:
            import sys as _sys
            print(
                f"[WARN] GPG signature verification {status} for manifest.json: {detail}",
                file=_sys.stderr,
            )
        return ok
    except Exception as exc:
        if audit_log_fn:
            audit_log_fn("gpg_verify", "error", str(exc))
        return False
