# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Commit signature scanner.

Scans commits to verify they have valid GPG or SSH signatures.
Used for enforcing commit signing policies.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from typing import Any

from mergeguide.hooks.git_utils import (
    GitError,
    SignatureStatus,
    get_commit_signature_info,
    get_commits_in_range,
    verify_commit_signature,
)

logger = logging.getLogger(__name__)


class SignaturePolicyMode(StrEnum):
    """Commit signing enforcement modes for the PR Gate."""

    OFF = "off"
    WARN = "warn"
    BLOCK = "block"


@dataclass
class CommitSignatureConfig:
    """Configuration for commit signature verification in PR Gate.

    Attributes:
        mode: Enforcement mode (off/warn/block).
        require_trusted: Whether to require fully trusted keys (GPG only).
        allowed_algorithms: List of allowed signature algorithms.
    """

    mode: SignaturePolicyMode = SignaturePolicyMode.OFF
    require_trusted: bool = False
    allowed_algorithms: list[str] = field(default_factory=lambda: ["gpg", "ssh"])


@dataclass
class SignatureFinding:
    """A commit signature finding."""

    commit_sha: str
    signed: bool
    valid: bool
    status: str
    signer: str
    key_id: str
    algorithm: str
    trust_level: str
    error: str | None
    author: str = ""
    severity_level: str = "error"  # unsigned/invalid = error, untrusted = warning

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "commit_sha": self.commit_sha,
            "signed": self.signed,
            "valid": self.valid,
            "status": self.status,
            "signer": self.signer,
            "key_id": self.key_id,
            "algorithm": self.algorithm,
            "trust_level": self.trust_level,
            "error": self.error,
            "author": self.author,
            "severity_level": self.severity_level,
        }


@dataclass
class SignatureScanResult:
    """Result of a commit signature scan."""

    scan_id: str
    timestamp: str
    repository: str
    ref: str = ""
    base_ref: str = ""
    total_commits: int = 0
    signed_count: int = 0
    valid_count: int = 0
    unsigned_count: int = 0
    invalid_count: int = 0
    findings: list[SignatureFinding] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        """Check if scan passed (all commits signed and valid)."""
        return self.unsigned_count == 0 and self.invalid_count == 0

    @property
    def all_signed(self) -> bool:
        """Check if all commits are signed (may include untrusted)."""
        return self.unsigned_count == 0

    @property
    def signing_rate(self) -> float:
        """Percentage of commits that are signed."""
        if self.total_commits == 0:
            return 100.0
        return (self.signed_count / self.total_commits) * 100

    @property
    def validity_rate(self) -> float:
        """Percentage of signed commits that are valid."""
        if self.signed_count == 0:
            return 0.0
        return (self.valid_count / self.signed_count) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "scan_id": self.scan_id,
            "timestamp": self.timestamp,
            "repository": self.repository,
            "ref": self.ref,
            "base_ref": self.base_ref,
            "passed": self.passed,
            "all_signed": self.all_signed,
            "total_commits": self.total_commits,
            "signed_count": self.signed_count,
            "valid_count": self.valid_count,
            "unsigned_count": self.unsigned_count,
            "invalid_count": self.invalid_count,
            "signing_rate": round(self.signing_rate, 1),
            "validity_rate": round(self.validity_rate, 1),
            "findings": [f.to_dict() for f in self.findings],
            "errors": self.errors,
        }


class CommitSignatureScanner:
    """Scans commits for valid signatures."""

    def __init__(
        self,
        require_trusted: bool = False,
        allowed_algorithms: list[str] | None = None,
    ):
        """Initialize scanner.

        Args:
            require_trusted: Whether to require fully trusted keys (GPG only).
                If False, any valid signature is accepted.
            allowed_algorithms: List of allowed signature algorithms.
                Defaults to ["gpg", "ssh"]. Set to ["gpg"] to require GPG only.
        """
        self._require_trusted = require_trusted
        self._allowed_algorithms = allowed_algorithms or ["gpg", "ssh"]

    def scan_commit(
        self,
        commit_sha: str,
        repo_path: str | Path | None = None,
    ) -> SignatureFinding:
        """Scan a single commit for signature.

        Args:
            commit_sha: The SHA of the commit to verify.
            repo_path: Optional path to the repository.

        Returns:
            SignatureFinding with verification results.
        """
        # Get basic signature verification
        result = verify_commit_signature(commit_sha, repo_path)

        # Get additional commit info
        info = get_commit_signature_info(commit_sha, repo_path)

        # Determine severity level
        severity_level = "info"
        if not result["signed"]:
            severity_level = "error"
        elif not result["valid"]:
            if result["status"] == SignatureStatus.UNTRUSTED:
                severity_level = "warning"
            else:
                severity_level = "error"
        elif result["algorithm"] not in self._allowed_algorithms:
            severity_level = "warning"
            result["error"] = f"Algorithm '{result['algorithm']}' not in allowed list"

        return SignatureFinding(
            commit_sha=commit_sha,
            signed=result["signed"],
            valid=result["valid"],
            status=result["status"],
            signer=result["signer"],
            key_id=result["key_id"],
            algorithm=result["algorithm"],
            trust_level=result["trust_level"],
            error=result.get("error"),
            author=info.get("author", ""),
            severity_level=severity_level,
        )

    def scan_commits(
        self,
        commits: list[str],
        repo_path: str | Path | None = None,
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> SignatureScanResult:
        """Scan multiple commits for signatures.

        Args:
            commits: List of commit SHAs to verify.
            repo_path: Optional path to the repository.
            repository: Repository name for the scan result.
            ref: Git ref being scanned.
            scan_id: Optional scan ID.

        Returns:
            SignatureScanResult with all findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

        result = SignatureScanResult(
            scan_id=scan_id,
            timestamp=datetime.now(UTC).isoformat(),
            repository=repository,
            ref=ref,
            total_commits=len(commits),
        )

        for commit_sha in commits:
            try:
                finding = self.scan_commit(commit_sha, repo_path)
                result.findings.append(finding)

                # Update counts
                if finding.signed:
                    result.signed_count += 1
                    if finding.valid:
                        result.valid_count += 1
                    else:
                        result.invalid_count += 1
                else:
                    result.unsigned_count += 1

            except Exception as e:
                logger.warning(f"Error scanning commit {commit_sha}: {e}")
                result.errors.append(f"Error scanning {commit_sha}: {str(e)}")

        return result

    def scan_range(
        self,
        base_sha: str,
        head_sha: str,
        repo_path: str | Path | None = None,
        repository: str = "",
        scan_id: str | None = None,
    ) -> SignatureScanResult:
        """Scan all commits in a range for signatures.

        Args:
            base_sha: The base commit (exclusive).
            head_sha: The head commit (inclusive).
            repo_path: Optional path to the repository.
            repository: Repository name.
            scan_id: Optional scan ID.

        Returns:
            SignatureScanResult with all findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

        result = SignatureScanResult(
            scan_id=scan_id,
            timestamp=datetime.now(UTC).isoformat(),
            repository=repository,
            ref=head_sha,
            base_ref=base_sha,
        )

        try:
            commits = get_commits_in_range(base_sha, head_sha, repo_path)
        except GitError as e:
            result.errors.append(str(e))
            return result

        result.total_commits = len(commits)

        for commit_sha in commits:
            try:
                finding = self.scan_commit(commit_sha, repo_path)
                result.findings.append(finding)

                if finding.signed:
                    result.signed_count += 1
                    if finding.valid:
                        result.valid_count += 1
                    else:
                        result.invalid_count += 1
                else:
                    result.unsigned_count += 1

            except Exception as e:
                logger.warning(f"Error scanning commit {commit_sha}: {e}")
                result.errors.append(f"Error scanning {commit_sha}: {str(e)}")

        return result

    def _generate_scan_id(self) -> str:
        """Generate a unique scan ID."""
        import uuid

        return f"sig_scan_{uuid.uuid4().hex[:12]}"


def scan_commit_signatures(
    commits: list[str],
    repo_path: str | Path | None = None,
    repository: str = "",
    require_trusted: bool = False,
) -> SignatureScanResult:
    """Convenience function to scan commit signatures.

    Args:
        commits: List of commit SHAs to verify.
        repo_path: Optional path to the repository.
        repository: Repository name.
        require_trusted: Whether to require trusted keys.

    Returns:
        SignatureScanResult with all findings.
    """
    scanner = CommitSignatureScanner(require_trusted=require_trusted)
    return scanner.scan_commits(commits, repo_path, repository=repository)


def scan_signature_range(
    base_sha: str,
    head_sha: str,
    repo_path: str | Path | None = None,
    repository: str = "",
    require_trusted: bool = False,
) -> SignatureScanResult:
    """Convenience function to scan signatures in a commit range.

    Args:
        base_sha: The base commit (exclusive).
        head_sha: The head commit (inclusive).
        repo_path: Optional path to the repository.
        repository: Repository name.
        require_trusted: Whether to require trusted keys.

    Returns:
        SignatureScanResult with all findings.
    """
    scanner = CommitSignatureScanner(require_trusted=require_trusted)
    return scanner.scan_range(base_sha, head_sha, repo_path, repository=repository)
