"""Upload attestations to Gjalla cloud."""

import hashlib
import json
import re
import subprocess
from pathlib import Path

import click
import httpx
import yaml

from ..config.settings import Settings
from ..display.output import console, info, success, warning, error
from ..protocol.http_client import fetch_context, fetch_element_tree
from ._cache import write_cache_files
from ._api import _make_api_headers
from ._shared import get_repo_root, load_local_config, local_config_exists, strip_yaml_delimiters

_GIT_HASH_RE = re.compile(r"^[0-9a-f]{40}([0-9a-f]{24})?$")

# SHA-1 of git's empty tree — used to diff root commits that have no parent.
# Computed via: git hash-object -t tree /dev/null
_GIT_EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"


def _parse_attestation_yaml(content: str) -> dict:
    """Parse attestation YAML frontmatter, stripping --- delimiters."""
    try:
        return yaml.safe_load(strip_yaml_delimiters(content)) or {}
    except yaml.YAMLError:
        return {}


def _is_valid_commit_hash(value: str) -> bool:
    """Return True if *value* looks like a full git SHA-1 (40 hex) or SHA-256 (64 hex)."""
    return bool(_GIT_HASH_RE.match(value))


def _find_commit_by_diff_hash(diff_hash: str, max_commits: int = 20) -> str | None:
    """Walk recent commits and return the one whose diff matches *diff_hash*.

    The attestation's ``staged_diff_hash`` is ``git diff --staged | shasum -a 256``.
    After a commit, the equivalent is ``git diff-tree -p <commit>`` which produces
    the same patch output as ``git diff --staged`` did at commit time.
    """
    if not diff_hash:
        return None
    try:
        result = subprocess.run(
            ["git", "log", "--format=%H", f"-{max_commits}"],
            capture_output=True, text=True,
        )
        if result.returncode != 0:
            return None
    except OSError:
        return None

    for commit in result.stdout.strip().splitlines():
        try:
            diff_result = subprocess.run(
                ["git", "diff-tree", "-p", "--no-commit-id", commit],
                capture_output=True, text=True,
            )
            if diff_result.returncode != 0:
                continue
            diff_output = diff_result.stdout
            # diff-tree -p on a root commit produces no output; diff against empty tree
            if not diff_output.strip():
                diff_result = subprocess.run(
                    ["git", "diff", _GIT_EMPTY_TREE_SHA, commit],
                    capture_output=True, text=True,
                )
                if diff_result.returncode != 0:
                    continue
                diff_output = diff_result.stdout
        except OSError:
            continue

        computed = hashlib.sha256(diff_output.encode()).hexdigest()
        if computed == diff_hash:
            return commit

    return None


def _process_pending_attestation(repo_root: Path) -> None:
    """Process any pending attestation file: log it, archive it, clean up.

    This runs unconditionally (even in local-only mode) so that attestations
    left behind by ``git commit --no-verify`` are always processed.

    To correctly associate the attestation with the right commit, we match
    the ``staged_diff_hash`` against recent commits.  Falls back to HEAD
    if no match is found.
    """
    attestation_path = repo_root / ".gjalla" / ".commit-attestation.yaml"
    if not attestation_path.exists():
        return

    gjallalog_path = repo_root / ".gjalla" / "log.jsonl"
    attestations_dir = repo_root / ".gjalla" / "attestations"

    # Migrate legacy .gjallalog to .gjalla/log.jsonl
    legacy_log = repo_root / ".gjallalog"
    if legacy_log.exists():
        with open(gjallalog_path, "a", encoding="utf-8") as f:
            f.write(legacy_log.read_text(encoding="utf-8"))
        legacy_log.unlink()
        info("Migrated .gjallalog → .gjalla/log.jsonl")

    # Parse once and reuse
    content = attestation_path.read_text(encoding="utf-8")
    parsed = _parse_attestation_yaml(content)

    # Try to find the exact commit this attestation belongs to
    diff_hash = parsed.get("staged_diff_hash", "")
    matched_commit = _find_commit_by_diff_hash(diff_hash)

    if matched_commit:
        info(f"Matched attestation to commit {matched_commit[:12]} via diff hash")
    else:
        warning("Could not match attestation to a recent commit — associating with HEAD")

    _log_attestation(gjallalog_path, attestations_dir, parsed, commit_override=matched_commit)
    attestation_path.unlink()
    info("Logged attestation and cleaned up .gjalla/.commit-attestation.yaml")


def _upload_attestations(settings: Settings, repo_root: Path, attestations_dir: Path) -> None:
    """Upload attestations to gjalla cloud."""
    if not local_config_exists(repo_root):
        # Check old format (.gjalla as a file)
        old_config = repo_root / ".gjalla"
        if not old_config.is_file():
            info("No .gjalla config — skipping attestation upload")
            return

    config = load_local_config(repo_root)
    if not config:
        warning("Invalid .gjalla config — skipping attestation upload")
        return

    project_id = config.get("project_id")
    api_url = config.get("api_url", settings.api_url)

    if not project_id:
        warning("No project_id in .gjalla config — attestations logged locally only")
        return

    # Upload unsent log entries
    gjallalog_path = repo_root / ".gjalla" / "log.jsonl"
    if not gjallalog_path.exists():
        info("No attestation log entries to upload")
        return

    api_key = settings.api_key
    if not api_key:
        warning("No API key configured — attestations logged locally only")
        return

    lines = gjallalog_path.read_text(encoding="utf-8").strip().splitlines()
    unsent = []
    skipped = 0
    for line in lines:
        try:
            entry = json.loads(line)
            # Backward compat: handle both "uploaded" and "synced" keys
            if not entry.get("uploaded") and not entry.get("synced"):
                commit = entry.get("commit", "")
                if not _is_valid_commit_hash(commit):
                    skipped += 1
                    continue
                # For new-format entries (no embedded attestation), load from file
                if "attestation" not in entry and "raw_attestation" not in entry:
                    att_file = attestations_dir / f"{commit}.yaml"
                    if att_file.exists():
                        docs = list(yaml.safe_load_all(att_file.read_text(encoding="utf-8")))
                        entry["attestation"] = docs[0] if docs else {}
                unsent.append(entry)
        except json.JSONDecodeError:
            pass

    if skipped:
        warning(f"Skipped {skipped} attestation(s) with invalid commit hashes")

    if not unsent:
        info("All attestations already uploaded")
        return

    info(f"Uploading {len(unsent)} attestation(s)...")

    try:
        response = httpx.post(
            f"{api_url}/api/agent/projects/{project_id}/attestations",
            headers=_make_api_headers(api_key),
            json={"attestations": unsent},
            timeout=30.0,
        )

        if response.status_code == 200:
            data = response.json()
            inserted = data.get("inserted", 0)
            success(f"Uploaded {inserted} attestation(s)")

            # Mark all as uploaded
            updated_lines = []
            for line in lines:
                try:
                    entry = json.loads(line)
                    entry["uploaded"] = True
                    entry["synced"] = True
                    updated_lines.append(json.dumps(entry))
                except json.JSONDecodeError:
                    updated_lines.append(line)
            gjallalog_path.write_text("\n".join(updated_lines) + "\n", encoding="utf-8")
        else:
            warning(f"Upload failed (HTTP {response.status_code}). Entries saved locally.")
    except httpx.RequestError as e:
        warning(f"Upload failed: {e}. Entries saved locally.")


def _log_attestation(
    gjallalog_path: Path,
    attestations_dir: Path,
    parsed: dict,
    commit_override: str | None = None,
) -> None:
    """Save full attestation to file and append minimal entry to log.

    Args:
        parsed: Already-parsed attestation dict.
        commit_override: If provided, use this commit hash instead of HEAD.
    """
    # Get commit hash and branch
    if commit_override:
        commit = commit_override
    else:
        try:
            result = subprocess.run(
                ["git", "rev-parse", "HEAD"],
                capture_output=True, text=True,
            )
            commit = result.stdout.strip() if result.returncode == 0 else "unknown"
        except OSError:
            commit = "unknown"

    try:
        result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True, text=True,
        )
        branch = result.stdout.strip() if result.returncode == 0 else "unknown"
    except OSError:
        branch = "unknown"

    # Save full attestation to individual YAML file
    attestations_dir.mkdir(parents=True, exist_ok=True)
    att_file = attestations_dir / f"{commit}.yaml"
    att_file.write_text(yaml.dump(parsed, default_flow_style=False, sort_keys=False), encoding="utf-8")

    # Append minimal entry to .gjalla/log.jsonl
    entry = {
        "commit": commit,
        "branch": branch,
        "timestamp": parsed.get("timestamp", ""),
        "agent": parsed.get("agent", ""),
        "summary": parsed.get("summary", ""),
        "synced": False,
    }
    if parsed.get("agent_id"):
        entry["agent_id"] = parsed["agent_id"]

    with open(gjallalog_path, "a") as f:
        f.write(json.dumps(entry, default=str) + "\n")


def _refresh_context_cache(settings: Settings, repo_root: Path) -> None:
    """Fetch project context from API and write local cache files."""
    config = load_local_config(repo_root)
    if not config:
        return

    project_id = config.get("project_id")

    if not project_id:
        return

    api_url = config.get("api_url", settings.api_url)

    console.print()
    info("[bold]Refreshing project context cache...[/bold]")

    try:
        context = fetch_context(project_id, settings.api_key, api_url)
        element_tree = fetch_element_tree(project_id, settings.api_key, api_url)
        write_cache_files(repo_root, context, element_tree=element_tree)
        success("Project context cached to .gjalla/cache/")
    except RuntimeError as e:
        warning(f"Could not refresh context cache: {e}")


@click.command()
def sync() -> None:
    """Upload pending attestations to gjalla cloud.

    Reads the local .gjalla/log.jsonl file and uploads any entries
    that haven't been sent yet.

    \b
    Examples:
        gjalla sync    # Upload pending attestations
    """
    repo_root = get_repo_root()

    console.print("[bold]gjalla Sync[/bold]")
    console.print()

    # Always process any pending attestation first (even in local-only mode)
    _process_pending_attestation(repo_root)

    # Check for local-only mode
    config = load_local_config(repo_root)
    local_arch_path = config.get("local_arch_path")
    local_rules_path = config.get("local_rules_path")
    # Backward compat: fall back to legacy single-path key
    if not local_arch_path and not local_rules_path:
        legacy = config.get("local_docs_path")
        if legacy:
            local_arch_path = legacy
            local_rules_path = legacy
    project_id = config.get("project_id")

    if (local_arch_path or local_rules_path) and not project_id:
        from pathlib import Path as _Path
        for label, p in [("Architecture", local_arch_path), ("Rules", local_rules_path)]:
            if not p:
                continue
            if _Path(p).exists():
                success(f"Local-only mode — {label.lower()} path valid: {p}")
            else:
                warning(f"Local {label.lower()} path no longer exists: {p}")
                info("Run [bold]gjalla setup[/bold] to update the path.")
        info("No remote project configured. Run [bold]gjalla connect[/bold] to link one.")
        return

    settings = Settings.load()

    # Prefer local project config for API key and URL over global config
    from ._shared import resolve_local_api_key
    local_key, local_url = resolve_local_api_key(repo_root)
    updates = {}
    if local_key:
        updates["api_key"] = local_key
    if local_url:
        updates["api_url"] = local_url
    if updates:
        settings = settings.model_copy(update=updates)

    if not settings.api_key:
        error("API key not configured. Run 'gjalla init' or 'gjalla connect' first.")
        raise SystemExit(1)

    attestations_dir = repo_root / ".gjalla" / "attestations"
    info("[bold]Uploading attestations...[/bold]")
    _upload_attestations(settings, repo_root, attestations_dir)

    # Refresh project context cache
    _refresh_context_cache(settings, repo_root)
