"""
AIIR internal — AI signal detection.

Copyright 2025-2026 Invariant Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""

from __future__ import annotations

import re
import subprocess
from typing import Dict, List, Optional

from aiir._core import (
    CommitInfo,
    GIT_TIMEOUT,
    _GIT_SAFE_ENV,
    _hash_diff_streaming,
    _normalize_for_detection,
    _run_git,
    _validate_ref,
)

# Git trailer keys that indicate AI involvement
AI_TRAILERS = [
    "generated-by",
    "ai-assisted",
    "ai-model",
    "tool",
    "copilot",
]


# Known AI authorship signals in commit messages (all lowercase for matching)
AI_SIGNALS = [
    "co-authored-by: copilot",
    "co-authored-by: github copilot",
    "co-authored-by: chatgpt",
    "co-authored-by: claude",
    "co-authored-by: cursor",
    "generated by copilot",
    "generated by chatgpt",
    "generated by claude",
    "generated by cursor",
    "generated by ai",
    "ai-generated",
    "llm-generated",
    "machine-generated",
    "aider:",
    "codeium",
    "github copilot",
    "cursor tab",
    "claude code",
    "windsurf",
    "sourcegraph cody",
    # Major AI coding tools that were missing from detection.
    # Amazon Q / CodeWhisperer, Devin, Google Gemini, Tabnine — top-10 by
    # market share.  A tool claiming AI detection that misses these is an
    # immediate credibility problem at launch.
    "amazon q",
    "amazon-q",
    "codewhisperer",
    "amazon codewhisperer",
    "co-authored-by: devin",
    "generated by devin",
    "devin ai",
    "gemini code assist",
    "google gemini",
    "tabnine",
    "codegen by gemini",
]


def detect_ai_signals(
    message: str,
    author_name: str = "",
    author_email: str = "",
    committer_name: str = "",
    committer_email: str = "",
) -> tuple:
    """Detect AI authorship and automation/bot signals in commit metadata.

    Returns a tuple of (ai_signals, bot_signals) where each is a list of
    detected signal descriptions.

    **AI signals** = declared AI coding assistance (Copilot, ChatGPT, etc.)
    **Bot signals** = pure automation (Dependabot, Renovate, github-actions, etc.)

    This distinction matters: ``is_ai_authored`` should only be True when
    AI tooling was involved, not for every automated commit.
    """
    ai_signals = []
    bot_signals = []
    # Use shared _normalize_for_detection helper for consistent
    # normalization across message body and author/committer fields.
    msg_clean = _normalize_for_detection(message)
    # Collapse whitespace runs (tab, double-space, etc.) to single
    # space so "Co-authored-by:\tCopilot" matches the canonical signal.
    msg_lower = re.sub(r'\s+', ' ', msg_clean).lower()

    # Check message body for AI signals
    for signal in AI_SIGNALS:
        if signal in msg_lower:
            ai_signals.append(f"message_match:{signal}")

    # Check for AI-related trailers (git trailer format: Key: Value)
    # Only record trailer key, not value (avoids leaking internal tool names)
    # S2: Normalize each line before matching to prevent Unicode homoglyph bypass
    # (e.g., Cyrillic 'е' in "Generated-by:" evading detection).
    for line in message.split("\n"):
        line_normalized = _normalize_for_detection(line).strip().lower()
        for trailer in AI_TRAILERS:
            if line_normalized.startswith(f"{trailer}:"):
                ai_signals.append(f"trailer:{trailer}")

    # Check author/committer for known AI agent patterns
    # These are AI-powered tools that act as commit authors.
    ai_author_patterns = [
        "copilot",
        "coderabbit",
        "devin[bot]",
        "devin-bot",
        "amazon-q",
        "tabnine",
        "gemini[bot]",
        "gemini-bot",
    ]
    # Pure automation bots — NOT AI.  These generate commits via
    # dependency updates, CI pipelines, or static analysis, not via
    # language model inference.
    bot_patterns = [
        "github-actions",
        "dependabot",
        "renovate",
        "snyk-bot",
        "deepsource",
    ]
    for name_field, label in [
        (author_name, "author_name"),
        (author_email, "author_email"),
        (committer_name, "committer_name"),
        (committer_email, "committer_email"),
    ]:
        # Use shared helper (consistent with message body path).
        clean_field = _normalize_for_detection(name_field or "").lower()
        for pattern in ai_author_patterns:
            if pattern in clean_field:
                ai_signals.append(f"{label}_bot:{pattern}")
        for pattern in bot_patterns:
            if pattern in clean_field:
                bot_signals.append(f"{label}_bot:{pattern}")

    return ai_signals, bot_signals


def get_commit_info(
    commit_ref: str = "HEAD", cwd: Optional[str] = None
) -> CommitInfo:
    """Parse full commit metadata from git."""
    _validate_ref(commit_ref)  # Reject option-like refs

    # Use NUL byte delimiter (%x00) instead of pipe.
    # Pipe characters can appear in author names and would shift all fields.
    fmt = "%H%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%s"
    # --no-mailmap prevents .mailmap from rewriting bot/AI author
    # identities to human emails, which would bypass AI detection heuristics.
    line = _run_git(["log", "-1", "--no-mailmap", f"--format={fmt}", commit_ref], cwd=cwd).strip()
    parts = line.split("\x00", 7)
    if len(parts) < 8:  # pragma: no cover
        raise ValueError(f"Failed to parse commit {commit_ref}: got {line!r}")

    sha, a_name, a_email, a_date, c_name, c_email, c_date, subject = parts

    # Validate SHA format — a corrupted repo or malicious git
    # wrapper could return non-hex data that propagates into receipts.
    if not re.match(r'^[0-9a-f]{40,64}$', sha):
        raise ValueError(f"Invalid commit SHA format: {sha[:80]!r}")

    # Full commit message body
    body = _run_git(["log", "-1", "--no-mailmap", "--format=%B", commit_ref], cwd=cwd).strip()

    # Handle root commits (no parent) by falling back to empty tree
    import os
    try:
        parent = _run_git(
            ["rev-parse", "--verify", f"{sha}~1"], cwd=cwd
        ).strip()
    except RuntimeError:
        # Root commit — compute empty tree hash dynamically
        # (works with both SHA-1 and SHA-256 repos)
        try:
            result = subprocess.run(
                ["git", "--no-optional-locks", "hash-object", "-t", "tree", "--stdin"],
                cwd=cwd or os.getcwd(),
                input=b"",
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=True,
                timeout=GIT_TIMEOUT,  # Prevent hang on corrupt git
                env=_GIT_SAFE_ENV,    # Consistent with _run_git
            )
            parent = result.stdout.decode().strip()
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:  # pragma: no cover
            raise RuntimeError(f"Failed to compute empty tree hash: {e}") from e

    # Diff stat
    # --no-ext-diff/--no-textconv prevent custom diff driver injection
    diff_stat = _run_git(
        ["diff", "--no-ext-diff", "--no-textconv", "--stat", parent, sha], cwd=cwd
    ).strip()

    # Full diff for hashing — stream to avoid OOM on huge diffs
    diff_hash = _hash_diff_streaming(parent, sha, cwd=cwd)

    # Changed files
    files_raw = _run_git(
        ["diff", "--no-ext-diff", "--no-textconv", "--name-only", parent, sha], cwd=cwd
    ).strip()
    files_changed = [f for f in files_raw.split("\n") if f.strip()]

    # Detect AI signals
    ai_signals, bot_signals = detect_ai_signals(body, a_name, a_email, c_name, c_email)

    # Derive structured authorship class from boolean pair
    is_ai = len(ai_signals) > 0
    is_bot = len(bot_signals) > 0
    if is_ai and is_bot:
        authorship_class = "ai+bot"
    elif is_ai:
        authorship_class = "ai-assisted"
    elif is_bot:
        authorship_class = "bot-generated"
    else:
        authorship_class = "human"

    return CommitInfo(
        sha=sha,
        author_name=a_name,
        author_email=a_email,
        author_date=a_date,
        committer_name=c_name,
        committer_email=c_email,
        committer_date=c_date,
        subject=subject,
        body=body,
        diff_stat=diff_stat,
        diff_hash=diff_hash,
        files_changed=files_changed,
        ai_signals_detected=ai_signals,
        is_ai_authored=is_ai,
        bot_signals_detected=bot_signals,
        is_bot_authored=is_bot,
        authorship_class=authorship_class,
    )


def list_commits_in_range(
    range_spec: str,
    cwd: Optional[str] = None,
    max_count: int = 1000,
) -> List[str]:
    """List commit SHAs in a range (e.g., 'origin/main..HEAD').

    Limited to max_count commits to prevent DoS.
    """
    if max_count < 1:
        raise ValueError(f"max_count must be positive, got {max_count}")
    _validate_ref(range_spec)  # Reject option-like refs
    output = _run_git(
        ["rev-list", f"--max-count={max_count}", range_spec], cwd=cwd
    ).strip()
    return [sha for sha in output.split("\n") if sha.strip()]
