"""Evidence storage backends for persisting evaluation artifacts.

This module provides storage implementations for MergeGuide evidence artifacts,
supporting both cloud-based (S3) and local filesystem storage. Evidence
artifacts are immutable records of policy evaluations used for audit trails
and compliance verification.

Storage Backends:
    - EvidenceStorage: Production S3-based storage with date-partitioned keys
    - LocalEvidenceStorage: Development/testing storage using local filesystem

Key Structure (S3):
    evidence/{org_id}/{repo}/{year}/{month}/{eval_id}.json

Example Usage:
    # S3 storage (production)
    storage = EvidenceStorage(bucket_name="my-evidence-bucket")
    key = storage.store(artifact, org_id="acme-corp", repo="api-server")
    retrieved = storage.retrieve(key)

    # Local storage (development)
    local_storage = LocalEvidenceStorage(base_path=".mergeguide/evidence")
    path = local_storage.store(artifact)

Note:
    S3 client is lazily initialized to avoid importing boto3 when not needed.
"""

from __future__ import annotations

import logging
from datetime import datetime
from typing import TYPE_CHECKING, Any

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from .artifact import EvidenceArtifact


class EvidenceStorageError(Exception):
    """Exception raised when evidence storage or retrieval operations fail.

    This exception wraps underlying storage errors (S3 errors, file I/O errors)
    to provide a consistent interface for error handling.

    Attributes:
        message: Human-readable description of the error.
    """

    pass


class EvidenceStorage:
    """Store evidence artifacts in Amazon S3 for production use.

    This class provides a durable, scalable storage backend for evidence
    artifacts using AWS S3. Artifacts are organized using a date-partitioned
    key structure that enables efficient querying by organization, repository,
    and time period.

    Key Structure:
        evidence/{org_id}/{repo}/{year}/{month}/{eval_id}.json

    The S3 objects include metadata headers for quick filtering without
    downloading the full artifact:
        - artifact-id: Unique evaluation ID
        - artifact-type: Always "sentinel_evaluation"
        - schema-version: Schema version for compatibility
        - org-id: Organization identifier
        - repo: Repository name
        - passed: "true" or "false" evaluation result

    Example:
        >>> storage = EvidenceStorage(bucket_name="evidence-bucket")
        >>> key = storage.store(artifact, org_id="acme", repo="api")
        >>> print(key)
        'evidence/acme/api/2025/01/eval-abc123.json'
        >>> retrieved = storage.retrieve(key)

    Attributes:
        bucket_name: The S3 bucket name for storage.
        region: AWS region where the bucket is located.
    """

    def __init__(
        self,
        bucket_name: str,
        region: str = "us-east-1",
    ) -> None:
        """Initialize evidence storage with S3 configuration.

        Args:
            bucket_name: Name of the S3 bucket for storing evidence.
                Must already exist and be accessible with current AWS credentials.
            region: AWS region where the bucket is located. Defaults to us-east-1.
        """
        self.bucket_name = bucket_name
        self.region = region
        self._s3 = None

    @property
    def s3(self) -> Any:
        """Get the S3 client, initializing lazily on first access.

        Lazy initialization avoids importing boto3 until actually needed,
        which improves startup time when S3 storage is not used.

        Returns:
            Configured boto3 S3 client instance.
        """
        if self._s3 is None:
            import boto3

            self._s3 = boto3.client("s3", region_name=self.region)
        return self._s3

    def store(
        self,
        artifact: EvidenceArtifact,
        org_id: str = "default",
        repo: str = "unknown",
    ) -> str:
        """Store an evidence artifact in S3.

        Serializes the artifact to JSON and uploads it to S3 with a
        date-partitioned key structure. Metadata is attached to the S3
        object for efficient filtering without downloading the full artifact.

        Args:
            artifact: The evidence artifact to store. Must have a valid
                timestamp and id.
            org_id: Organization identifier for key partitioning. Used to
                isolate evidence by tenant. Defaults to "default".
            repo: Repository name for key partitioning. Defaults to "unknown".

        Returns:
            The S3 key where the artifact was stored, e.g.,
            "evidence/acme/api/2025/01/eval-abc123.json".

        Raises:
            EvidenceStorageError: If the S3 upload fails due to permissions,
                network issues, or bucket configuration problems.
        """
        # Parse timestamp to extract year/month for key partitioning.
        # Falls back to current time if timestamp parsing fails.
        try:
            ts = datetime.fromisoformat(artifact.timestamp.replace("Z", "+00:00"))
        except ValueError:
            ts = datetime.utcnow()

        # Build the S3 key using date-based partitioning
        key = self._build_key(
            org_id=org_id,
            repo=repo,
            year=ts.year,
            month=ts.month,
            eval_id=artifact.id,
        )

        # Serialize artifact to JSON format
        content = artifact.to_json()

        try:
            # Upload with metadata for efficient filtering/querying
            self.s3.put_object(
                Bucket=self.bucket_name,
                Key=key,
                Body=content.encode("utf-8"),
                ContentType="application/json",
                Metadata={
                    "artifact-id": artifact.id,
                    "artifact-type": artifact.artifact_type,
                    "schema-version": artifact.schema_version,
                    "org-id": org_id,
                    "repo": repo,
                    "passed": str(artifact.evaluation.passed).lower(),
                },
            )
        except Exception as e:
            raise EvidenceStorageError(f"Failed to store artifact: {e}") from e

        return key

    def retrieve(self, key: str) -> EvidenceArtifact:
        """Retrieve an evidence artifact from S3 by its key.

        Downloads the JSON artifact from S3 and deserializes it back into
        an EvidenceArtifact object.

        Args:
            key: Full S3 key of the artifact, e.g.,
                "evidence/acme/api/2025/01/eval-abc123.json".

        Returns:
            Deserialized EvidenceArtifact instance.

        Raises:
            EvidenceStorageError: If the artifact does not exist, access is
                denied, or there are network/parsing errors.
        """
        from .artifact import EvidenceArtifact

        try:
            response = self.s3.get_object(
                Bucket=self.bucket_name,
                Key=key,
            )
            content = response["Body"].read().decode("utf-8")
            return EvidenceArtifact.from_json(content)
        except Exception as e:
            raise EvidenceStorageError(f"Failed to retrieve artifact: {e}") from e

    def retrieve_by_id(
        self,
        eval_id: str,
        org_id: str = "default",
        repo: str = "unknown",
    ) -> EvidenceArtifact:
        """Retrieve an evidence artifact by evaluation ID.

        Searches for an artifact by its evaluation ID when the full S3 key
        is not known. Since artifacts are date-partitioned, this method
        searches backwards through recent months to locate the artifact.

        Note:
            This operation is less efficient than retrieve() since it
            requires listing and scanning S3 objects. For production use,
            consider maintaining an index or caching full keys.

        Args:
            eval_id: The unique evaluation ID (e.g., "eval-abc123").
            org_id: Organization identifier to narrow the search scope.
                Defaults to "default".
            repo: Repository name to narrow the search scope.
                Defaults to "unknown".

        Returns:
            The matching EvidenceArtifact if found.

        Raises:
            EvidenceStorageError: If the artifact is not found in the last
                3 months or if there are S3 access errors.
        """
        from datetime import datetime, timedelta

        # Search backwards through recent months to find the artifact.
        # This is a trade-off between efficiency and usability when the
        # exact storage date is unknown.
        now = datetime.utcnow()
        for months_ago in range(3):
            dt = now - timedelta(days=30 * months_ago)
            prefix = f"evidence/{org_id}/{repo}/{dt.year}/{dt.month:02d}/"

            try:
                response = self.s3.list_objects_v2(
                    Bucket=self.bucket_name,
                    Prefix=prefix,
                )

                # Scan objects in this month's partition for matching eval_id
                for obj in response.get("Contents", []):
                    if eval_id in obj["Key"]:
                        return self.retrieve(obj["Key"])

            except Exception as e:
                logger.debug(f"Search prefix {prefix} failed: {e}")
                continue

        raise EvidenceStorageError(f"Artifact not found: {eval_id}")

    def list_artifacts(
        self,
        org_id: str = "default",
        repo: str | None = None,
        year: int | None = None,
        month: int | None = None,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        """List evidence artifacts with optional filtering.

        Returns metadata about stored artifacts without downloading the full
        artifact contents. Filters can be combined to narrow the search scope
        (more specific filters = faster queries).

        Note:
            This method makes HEAD requests for each object to retrieve
            metadata, which can be slow for large result sets. Consider
            using more specific filters to limit results.

        Args:
            org_id: Organization identifier to filter by. Required.
                Defaults to "default".
            repo: Optional repository name filter. When specified, only
                artifacts from this repository are returned.
            year: Optional year filter (e.g., 2025). Requires repo to be set.
            month: Optional month filter (1-12). Requires year to be set.
            limit: Maximum number of artifacts to return. Defaults to 100.

        Returns:
            List of dictionaries containing artifact metadata:
                - key: S3 key of the artifact
                - size: Size in bytes
                - last_modified: ISO timestamp of last modification
                - metadata: S3 object metadata (artifact-id, passed, etc.)

        Raises:
            EvidenceStorageError: If listing fails due to S3 access errors.
        """
        # Build S3 prefix based on provided filters.
        # More specific filters result in narrower S3 listing scope.
        prefix_parts = ["evidence", org_id]
        if repo:
            prefix_parts.append(repo)
            if year:
                prefix_parts.append(str(year))
                if month:
                    prefix_parts.append(f"{month:02d}")

        prefix = "/".join(prefix_parts) + "/"

        try:
            response = self.s3.list_objects_v2(
                Bucket=self.bucket_name,
                Prefix=prefix,
                MaxKeys=limit,
            )

            artifacts = []
            for obj in response.get("Contents", []):
                # Fetch object metadata via HEAD request
                head = self.s3.head_object(
                    Bucket=self.bucket_name,
                    Key=obj["Key"],
                )

                artifacts.append({
                    "key": obj["Key"],
                    "size": obj["Size"],
                    "last_modified": obj["LastModified"].isoformat(),
                    "metadata": head.get("Metadata", {}),
                })

            return artifacts

        except Exception as e:
            raise EvidenceStorageError(f"Failed to list artifacts: {e}") from e

    def _build_key(
        self,
        org_id: str,
        repo: str,
        year: int,
        month: int,
        eval_id: str,
    ) -> str:
        """Build the S3 key for storing an artifact.

        Creates a date-partitioned key that enables efficient querying by
        organization, repository, and time period.

        Args:
            org_id: Organization identifier.
            repo: Repository name.
            year: Year portion of the timestamp (e.g., 2025).
            month: Month portion of the timestamp (1-12).
            eval_id: Unique evaluation identifier.

        Returns:
            S3 key string in the format:
            "evidence/{org_id}/{repo}/{year}/{month:02d}/{eval_id}.json"
        """
        return f"evidence/{org_id}/{repo}/{year}/{month:02d}/{eval_id}.json"


class LocalEvidenceStorage:
    """Store evidence artifacts on the local filesystem.

    This storage backend is intended for development and testing scenarios
    where S3 access is not available or not desired. It mirrors the
    date-partitioned directory structure of the S3 backend.

    Directory Structure:
        {base_path}/{org_id}/{repo}/{year}/{month}/{eval_id}.json

    Example:
        >>> storage = LocalEvidenceStorage(base_path=".mergeguide/evidence")
        >>> path = storage.store(artifact, org_id="local", repo="test")
        >>> print(path)
        '.mergeguide/evidence/local/test/2025/01/eval-abc123.json'

    Note:
        This backend does not implement all methods of EvidenceStorage
        (e.g., list_artifacts, retrieve_by_id). Use EvidenceStorage for
        production deployments requiring full functionality.

    Attributes:
        base_path: Root directory for storing evidence files.
    """

    def __init__(self, base_path: str = ".mergeguide/evidence") -> None:
        """Initialize local filesystem storage.

        Creates the base directory if it does not exist.

        Args:
            base_path: Root directory path for storing evidence artifacts.
                Relative paths are resolved from the current working directory.
                Defaults to ".mergeguide/evidence".
        """
        from pathlib import Path

        self.base_path = Path(base_path)
        self.base_path.mkdir(parents=True, exist_ok=True)

    def store(
        self,
        artifact: EvidenceArtifact,
        org_id: str = "default",
        repo: str = "unknown",
    ) -> str:
        """Store an evidence artifact to the local filesystem.

        Creates a date-partitioned directory structure mirroring the S3
        backend and writes the artifact as a JSON file.

        Args:
            artifact: The evidence artifact to store. Must have valid
                timestamp and id fields.
            org_id: Organization identifier for directory partitioning.
                Defaults to "default".
            repo: Repository name for directory partitioning.
                Defaults to "unknown".

        Returns:
            Absolute file path where the artifact was stored.
        """
        from datetime import datetime

        # Parse timestamp to extract year/month for directory partitioning.
        # Falls back to current time if timestamp parsing fails.
        try:
            ts = datetime.fromisoformat(artifact.timestamp.replace("Z", "+00:00"))
        except ValueError:
            ts = datetime.utcnow()

        # Build directory structure matching S3 key pattern
        artifact_dir = (
            self.base_path / org_id / repo / str(ts.year) / f"{ts.month:02d}"
        )
        artifact_dir.mkdir(parents=True, exist_ok=True)

        # Write artifact as JSON file
        artifact_path = artifact_dir / f"{artifact.id}.json"
        artifact_path.write_text(artifact.to_json())

        return str(artifact_path)

    def retrieve(self, path: str) -> EvidenceArtifact:
        """Retrieve an evidence artifact from the local filesystem.

        Reads and deserializes an artifact from the specified file path.

        Args:
            path: File path to the artifact JSON file. Can be absolute
                or relative to the current working directory.

        Returns:
            Deserialized EvidenceArtifact instance.

        Raises:
            EvidenceStorageError: If the file does not exist or cannot
                be read/parsed.
        """
        from pathlib import Path

        from .artifact import EvidenceArtifact

        artifact_path = Path(path)
        if not artifact_path.exists():
            raise EvidenceStorageError(f"Artifact not found: {path}")

        content = artifact_path.read_text()
        return EvidenceArtifact.from_json(content)
