"""Evidence artifact generation for audit compliance.

This module defines the data structures and serialization logic for MergeGuide
evidence artifacts. Evidence artifacts are immutable records of policy
evaluations that provide an audit trail for compliance verification.

Key Features:
    - Cryptographic integrity verification via SHA-256 hashes
    - HMAC-SHA256 signatures for non-repudiation
    - Complete evaluation context capture
    - JSON serialization for storage and transmission

Data Classes:
    - SourceInfo: Describes what was evaluated (file, PR, diff)
    - HashInfo: Cryptographic hashes for integrity verification
    - EvaluationResultInfo: Summary of evaluation results
    - ViolationInfo: Minimal violation data for the evidence record
    - EvidenceArtifact: Complete evidence artifact combining all components

Example Usage:
    # Create artifact from policy evaluation output
    artifact = EvidenceArtifact.from_output(
        output=policy_output,
        source=SourceInfo(type="pr", location="https://github.com/org/repo/pull/123"),
        context=evaluation_context,
        input_content=pr_diff,
        policies_used=["security-001", "quality-002"],
    )

    # Sign for non-repudiation
    artifact.sign(secret_key="signing-key")

    # Serialize for storage
    json_str = artifact.to_json()

    # Verify signature when retrieving
    loaded = EvidenceArtifact.from_json(json_str)
    is_valid = loaded.verify_signature(secret_key="signing-key")
"""

from __future__ import annotations

import hashlib
import hmac
import json
import uuid
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from typing import Any

from mergeguide.engine.evaluator import EvaluationContext, PolicyOutput
from mergeguide.engine.reporter import Violation


@dataclass
class SourceInfo:
    """Information about the source being evaluated.

    Captures metadata about what was evaluated, enabling traceability
    from the evidence artifact back to the original source.

    Attributes:
        type: Source type identifier. Common values:
            - "file": Single file evaluation
            - "diff": Git diff evaluation
            - "pr": Pull request evaluation
        location: Location reference (file path, PR URL, commit SHA, etc.).
        ref: Git reference if applicable (branch name, tag, commit SHA).
        size_bytes: Size of the evaluated content in bytes.

    Example:
        >>> source = SourceInfo(
        ...     type="pr",
        ...     location="https://github.com/acme/api/pull/42",
        ...     ref="feature/new-endpoint",
        ...     size_bytes=15234,
        ... )
    """

    type: str
    location: str
    ref: str = ""
    size_bytes: int = 0


@dataclass
class HashInfo:
    """Cryptographic hashes for integrity verification.

    Provides SHA-256 hashes of the input content, policies used, and
    evaluation output to enable tamper detection and integrity verification.

    Attributes:
        algorithm: Hash algorithm used (always "sha256").
        input_hash: SHA-256 hash of the input content that was evaluated.
        policy_hash: SHA-256 hash of the sorted, concatenated policy IDs.
        output_hash: SHA-256 hash of the serialized artifact (excluding signature).

    Example:
        >>> hashes = HashInfo()
        >>> hashes.compute_input_hash("file contents here")
        >>> print(hashes.input_hash)
        'a1b2c3d4...'
    """

    algorithm: str = "sha256"
    input_hash: str = ""
    policy_hash: str = ""
    output_hash: str = ""

    def compute_input_hash(self, content: str) -> None:
        """Compute and store the SHA-256 hash of input content.

        Args:
            content: The input content that was evaluated.
        """
        self.input_hash = hashlib.sha256(content.encode()).hexdigest()

    def compute_policy_hash(self, policies: list[str]) -> None:
        """Compute and store the SHA-256 hash of policy identifiers.

        Policies are sorted and joined with newlines before hashing
        to ensure consistent hashes regardless of input order.

        Args:
            policies: List of policy identifiers that were applied.
        """
        combined = "\n".join(sorted(policies))
        self.policy_hash = hashlib.sha256(combined.encode()).hexdigest()

    def compute_output_hash(self, output: str) -> None:
        """Compute and store the SHA-256 hash of the output.

        This is typically computed on the serialized artifact JSON
        (excluding the signature fields) to enable integrity verification.

        Args:
            output: The serialized output string to hash.
        """
        self.output_hash = hashlib.sha256(output.encode()).hexdigest()


@dataclass
class EvaluationResultInfo:
    """Summary of evaluation results.

    Provides aggregate statistics about the evaluation outcome without
    including the full violation details.

    Attributes:
        passed: Whether the evaluation passed (no errors).
        total_violations: Total count of all violations.
        errors: Count of error-severity violations.
        warnings: Count of warning-severity violations.
        infos: Count of info-severity violations.
        files_checked: List of file paths that were evaluated.

    Example:
        >>> result = EvaluationResultInfo(
        ...     passed=False,
        ...     total_violations=3,
        ...     errors=1,
        ...     warnings=2,
        ...     infos=0,
        ...     files_checked=["src/api.py", "src/utils.py"],
        ... )
    """

    passed: bool
    total_violations: int
    errors: int
    warnings: int
    infos: int
    files_checked: list[str] = field(default_factory=list)


@dataclass
class ViolationInfo:
    """Minimal violation information for evidence records.

    Contains only the essential violation data needed for audit trails,
    excluding verbose details like code snippets or fix suggestions.

    Attributes:
        rule_id: Identifier of the policy rule that was violated.
        severity: Violation severity ("error", "warning", or "info").
        file_path: Path to the file containing the violation.
        line_no: Line number where the violation was detected.
        message: Human-readable description of the violation.

    Example:
        >>> violation = ViolationInfo(
        ...     rule_id="SEC-001",
        ...     severity="error",
        ...     file_path="src/auth.py",
        ...     line_no=42,
        ...     message="Hardcoded credential detected",
        ... )
    """

    rule_id: str
    severity: str
    file_path: str
    line_no: int
    message: str

    @classmethod
    def from_violation(cls, violation: Violation) -> ViolationInfo:
        """Create a ViolationInfo from a full Violation object.

        Extracts only the fields needed for the evidence record.

        Args:
            violation: The full Violation object from evaluation.

        Returns:
            A new ViolationInfo instance with minimal fields.
        """
        return cls(
            rule_id=violation.rule_id,
            severity=violation.severity,
            file_path=violation.file_path,
            line_no=violation.line_no,
            message=violation.message,
        )


@dataclass
class EvidenceArtifact:
    """Complete evidence artifact for a policy evaluation.

    This is the primary data structure for MergeGuide's audit trail. Each
    artifact represents a single policy evaluation and contains:

    1. Immutable record of what was evaluated (source, context)
    2. Complete evaluation results (pass/fail, violations)
    3. Cryptographic hashes for integrity verification
    4. Optional HMAC signature for non-repudiation

    The artifact is designed to be:
    - Self-contained: All necessary context is embedded
    - Verifiable: Hashes and signatures enable tamper detection
    - Serializable: JSON format for storage and transmission
    - Compliant: Meets audit requirements for SOC2, ISO 27001, etc.

    Attributes:
        schema_version: Version of the artifact schema for compatibility.
        artifact_type: Type identifier (always "sentinel_evaluation").
        id: Unique identifier for this evaluation (auto-generated).
        timestamp: ISO 8601 timestamp of when the evaluation occurred.
        source: Information about what was evaluated.
        context: Additional evaluation context (user, environment, etc.).
        evaluation: Summary of evaluation results.
        violations: List of violations found during evaluation.
        hashes: Cryptographic hashes for integrity verification.
        duration_ms: Evaluation duration in milliseconds.
        policies_used: List of policy IDs that were applied.
        signature: HMAC signature for non-repudiation (optional).
        signature_algorithm: Algorithm used for signature (e.g., "hmac-sha256").

    Example:
        >>> artifact = EvidenceArtifact.from_output(
        ...     output=policy_output,
        ...     source=SourceInfo(type="pr", location="..."),
        ...     context=eval_context,
        ... )
        >>> artifact.sign("secret-key")
        >>> json_str = artifact.to_json()
    """

    # Schema and type metadata
    schema_version: str = "1.0"
    artifact_type: str = "sentinel_evaluation"

    # Unique identification
    id: str = field(default_factory=lambda: f"eval-{uuid.uuid4().hex[:12]}")
    timestamp: str = field(
        default_factory=lambda: datetime.now(UTC).isoformat()
    )

    # Source information - what was evaluated
    source: SourceInfo = field(default_factory=lambda: SourceInfo(type="unknown", location=""))

    # Evaluation context - who/where/why
    context: dict[str, Any] = field(default_factory=dict)

    # Evaluation result - pass/fail summary
    evaluation: EvaluationResultInfo = field(
        default_factory=lambda: EvaluationResultInfo(
            passed=True, total_violations=0, errors=0, warnings=0, infos=0
        )
    )

    # Violations list - minimal violation details
    violations: list[ViolationInfo] = field(default_factory=list)

    # Integrity hashes - SHA-256 hashes for verification
    hashes: HashInfo = field(default_factory=HashInfo)

    # Timing information
    duration_ms: int = 0

    # Policies used in evaluation
    policies_used: list[str] = field(default_factory=list)

    # Optional signature for non-repudiation
    signature: str = ""
    signature_algorithm: str = ""

    @classmethod
    def from_output(
        cls,
        output: PolicyOutput,
        source: SourceInfo,
        context: EvaluationContext,
        input_content: str = "",
        policies_used: list[str] | None = None,
    ) -> EvidenceArtifact:
        """Create an evidence artifact from a PolicyOutput.

        This is the primary factory method for creating artifacts after
        running a policy evaluation. It extracts all relevant data from
        the evaluation output and computes integrity hashes.

        Args:
            output: The PolicyOutput from running policy evaluation.
            source: SourceInfo describing what was evaluated.
            context: EvaluationContext with user/environment information.
            input_content: Original input content for hash computation.
                If empty, input_hash will not be computed.
            policies_used: List of policy IDs that were applied. Used
                to compute policy_hash for integrity verification.

        Returns:
            A fully populated EvidenceArtifact with computed hashes.
            The artifact is ready for optional signing and storage.

        Example:
            >>> artifact = EvidenceArtifact.from_output(
            ...     output=evaluator.evaluate(content),
            ...     source=SourceInfo(type="file", location="/path/to/file.py"),
            ...     context=EvaluationContext(user="developer@example.com"),
            ...     input_content=file_content,
            ...     policies_used=["SEC-001", "QUAL-002"],
            ... )
        """
        # Count violations by severity for the result summary
        errors = sum(1 for v in output.violations if v.severity == "error")
        warnings = sum(1 for v in output.violations if v.severity == "warning")
        infos = sum(1 for v in output.violations if v.severity == "info")

        evaluation = EvaluationResultInfo(
            passed=output.passed,
            total_violations=len(output.violations),
            errors=errors,
            warnings=warnings,
            infos=infos,
            files_checked=output.files_checked,
        )

        # Convert full Violation objects to minimal ViolationInfo
        violations = [
            ViolationInfo.from_violation(v) for v in output.violations
        ]

        # Compute integrity hashes for input and policies
        hashes = HashInfo()
        if input_content:
            hashes.compute_input_hash(input_content)
        if policies_used:
            hashes.compute_policy_hash(policies_used)

        artifact = cls(
            id=output.evaluation_id,
            source=source,
            context=asdict(context),
            evaluation=evaluation,
            violations=violations,
            hashes=hashes,
            duration_ms=output.duration_ms,
            policies_used=policies_used or [],
        )

        # Compute output hash on serialized artifact (excluding signature)
        # This enables verification that the artifact hasn't been tampered with
        artifact_json = artifact.to_json(include_signature=False)
        artifact.hashes.compute_output_hash(artifact_json)

        return artifact

    def sign(self, secret_key: str, algorithm: str = "hmac-sha256") -> None:
        """Sign the artifact for non-repudiation and integrity verification.

        Computes an HMAC signature over the canonical representation of
        the artifact. The signature can be verified later using the same
        secret key to ensure the artifact hasn't been tampered with.

        Note:
            The signature is computed over the JSON representation
            excluding the signature fields themselves. This ensures
            verification works correctly after serialization/deserialization.

        Args:
            secret_key: The secret key to use for HMAC computation.
                Should be stored securely and not embedded in code.
            algorithm: Signature algorithm. Only "hmac-sha256" is currently
                supported. Defaults to "hmac-sha256".

        Raises:
            ValueError: If an unsupported signature algorithm is specified.

        Example:
            >>> artifact.sign(secret_key=os.environ["SIGNING_KEY"])
            >>> print(artifact.signature)
            'a1b2c3d4...'
        """
        if algorithm != "hmac-sha256":
            raise ValueError(f"Unsupported signature algorithm: {algorithm}")

        # Create canonical representation (excludes signature fields)
        canonical = self._canonical_representation()

        # Compute HMAC-SHA256 signature
        signature = hmac.new(
            secret_key.encode(),
            canonical.encode(),
            hashlib.sha256,
        ).hexdigest()

        self.signature = signature
        self.signature_algorithm = algorithm

    def verify_signature(self, secret_key: str) -> bool:
        """Verify the artifact's HMAC signature.

        Recomputes the signature using the provided key and compares
        it to the stored signature using constant-time comparison to
        prevent timing attacks.

        Args:
            secret_key: The secret key to verify with. Must be the same
                key that was used to sign the artifact.

        Returns:
            True if the signature is valid and the artifact hasn't been
            tampered with. False if the signature is missing, uses an
            unsupported algorithm, or doesn't match.

        Example:
            >>> loaded = EvidenceArtifact.from_json(json_str)
            >>> if not loaded.verify_signature(secret_key):
            ...     raise SecurityError("Artifact signature verification failed")
        """
        if not self.signature:
            return False

        if self.signature_algorithm != "hmac-sha256":
            return False

        # Create canonical representation (same as signing)
        canonical = self._canonical_representation()

        # Compute expected signature
        expected = hmac.new(
            secret_key.encode(),
            canonical.encode(),
            hashlib.sha256,
        ).hexdigest()

        # Use constant-time comparison to prevent timing attacks
        return hmac.compare_digest(self.signature, expected)

    def _canonical_representation(self) -> str:
        """Create canonical JSON representation for signing.

        Returns the artifact serialized as JSON without signature fields.
        This ensures consistent signing/verification across serialization
        boundaries.

        Returns:
            JSON string representation excluding signature and
            signature_algorithm fields.
        """
        return self.to_json(include_signature=False)

    def to_dict(self, include_signature: bool = True) -> dict[str, Any]:
        """Convert the artifact to a dictionary representation.

        Args:
            include_signature: Whether to include signature fields in the
                output. Set to False when creating canonical representation
                for signing/verification. Defaults to True.

        Returns:
            Dictionary containing all artifact fields. Nested dataclasses
            (source, evaluation, violations, hashes) are converted to dicts.
        """
        data = {
            "schema_version": self.schema_version,
            "artifact_type": self.artifact_type,
            "id": self.id,
            "timestamp": self.timestamp,
            "source": asdict(self.source),
            "context": self.context,
            "evaluation": asdict(self.evaluation),
            "violations": [asdict(v) for v in self.violations],
            "hashes": asdict(self.hashes),
            "duration_ms": self.duration_ms,
            "policies_used": self.policies_used,
        }

        if include_signature and self.signature:
            data["signature"] = self.signature
            data["signature_algorithm"] = self.signature_algorithm

        return data

    def to_json(self, include_signature: bool = True, indent: int = 2) -> str:
        """Serialize the artifact to a JSON string.

        Args:
            include_signature: Whether to include signature fields.
                Set to False for canonical representation used in
                signing/verification. Defaults to True.
            indent: Number of spaces for JSON indentation.
                Use None for compact output. Defaults to 2.

        Returns:
            JSON string representation with keys sorted alphabetically
            for consistent output.
        """
        return json.dumps(
            self.to_dict(include_signature=include_signature),
            indent=indent,
            sort_keys=True,
        )

    @classmethod
    def from_json(cls, json_str: str) -> EvidenceArtifact:
        """Deserialize an artifact from a JSON string.

        Args:
            json_str: JSON string representation of an artifact.

        Returns:
            Reconstructed EvidenceArtifact instance.

        Raises:
            json.JSONDecodeError: If the string is not valid JSON.
            TypeError: If required fields are missing or have wrong types.
        """
        data = json.loads(json_str)
        return cls.from_dict(data)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> EvidenceArtifact:
        """Reconstruct an artifact from a dictionary.

        Handles missing fields gracefully by using defaults, enabling
        backward compatibility with older artifact versions.

        Args:
            data: Dictionary containing artifact fields. Nested structures
                (source, hashes, evaluation, violations) are expected as
                sub-dictionaries.

        Returns:
            Reconstructed EvidenceArtifact instance with all fields
            populated from the dictionary or defaults.
        """
        # Reconstruct nested dataclass instances from sub-dictionaries
        source = SourceInfo(**data.get("source", {}))
        hashes = HashInfo(**data.get("hashes", {}))
        evaluation = EvaluationResultInfo(**data.get("evaluation", {}))
        violations = [
            ViolationInfo(**v) for v in data.get("violations", [])
        ]

        return cls(
            schema_version=data.get("schema_version", "1.0"),
            artifact_type=data.get("artifact_type", "sentinel_evaluation"),
            id=data.get("id", ""),
            timestamp=data.get("timestamp", ""),
            source=source,
            context=data.get("context", {}),
            evaluation=evaluation,
            violations=violations,
            hashes=hashes,
            duration_ms=data.get("duration_ms", 0),
            policies_used=data.get("policies_used", []),
            signature=data.get("signature", ""),
            signature_algorithm=data.get("signature_algorithm", ""),
        )
