"""
OmniSec GCS — Google Cloud Storage Abstraction
================================================
High-level, ergonomic interface for Google Cloud Storage
with upload, download, listing, signed URLs, metadata,
lifecycle management, and resumable transfers.
"""

from __future__ import annotations

import os
import json
import hashlib
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Iterator
from omnisec.core.logger import get_logger

log = get_logger("omnisec.gcs")


class GCSClient:
    """
    Simplified Google Cloud Storage client.

    Wraps google-cloud-storage with:
      - Simple upload / download / delete operations
      - Directory-style recursive upload and download
      - Signed URL generation (GET/PUT)
      - Object metadata management
      - Bucket creation and policy management
      - Existence checks and size queries
      - MD5/CRC32C integrity verification

    Requires:
        pip install google-cloud-storage

    Example:
        >>> gcs = GCSClient(project="my-project", bucket="my-bucket")
        >>> gcs.upload("local/report.pdf", "reports/2024/report.pdf")
        >>> url = gcs.signed_url("reports/2024/report.pdf", expiry_minutes=60)
    """

    def __init__(
        self,
        project: Optional[str] = None,
        bucket: Optional[str] = None,
        credentials_path: Optional[str] = None,
        region: str = "us-central1",
    ):
        self.project = project or os.getenv("GOOGLE_CLOUD_PROJECT")
        self.bucket_name = bucket or os.getenv("OMNISEC_GCS_BUCKET")
        self.credentials_path = credentials_path or os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
        self.region = region
        self._client = None
        self._bucket = None
        self._init_client()

    def _init_client(self):
        try:
            from google.cloud import storage
            from google.oauth2 import service_account

            if self.credentials_path:
                creds = service_account.Credentials.from_service_account_file(
                    self.credentials_path
                )
                self._client = storage.Client(project=self.project, credentials=creds)
            else:
                self._client = storage.Client(project=self.project)

            if self.bucket_name:
                self._bucket = self._client.bucket(self.bucket_name)
            log.info(f"GCSClient initialized — project={self.project}, bucket={self.bucket_name}")
        except ImportError:
            log.warning("google-cloud-storage not installed. Run: pip install google-cloud-storage")

    def _require_client(self):
        if not self._client:
            raise RuntimeError("GCS client not available. Install 'google-cloud-storage'.")

    def _require_bucket(self):
        self._require_client()
        if not self._bucket:
            raise ValueError("No bucket specified. Pass bucket= to GCSClient().")

    # ── Bucket Operations ───────────────────────────────────────────

    def use_bucket(self, bucket_name: str) -> "GCSClient":
        """
        Switch to a different bucket.

        Returns:
            self (for chaining)

        Example:
            >>> gcs.use_bucket("archive-bucket").upload("file.txt", "file.txt")
        """
        self._require_client()
        self.bucket_name = bucket_name
        self._bucket = self._client.bucket(bucket_name)
        return self

    def create_bucket(
        self,
        bucket_name: str,
        location: Optional[str] = None,
        storage_class: str = "STANDARD",
    ) -> Any:
        """
        Create a new GCS bucket.

        Args:
            bucket_name:   Globally unique bucket name
            location:      Region (default: client region)
            storage_class: STANDARD | NEARLINE | COLDLINE | ARCHIVE

        Returns:
            google.cloud.storage.Bucket

        Example:
            >>> gcs.create_bucket("my-new-bucket", location="us-east1")
        """
        self._require_client()
        bucket = self._client.bucket(bucket_name)
        bucket.storage_class = storage_class
        new_bucket = self._client.create_bucket(bucket, location=location or self.region)
        log.info(f"Created bucket gs://{bucket_name} in {location or self.region}")
        return new_bucket

    def list_buckets(self) -> List[str]:
        """
        List all buckets in the project.

        Returns:
            List of bucket names

        Example:
            >>> for name in gcs.list_buckets(): print(name)
        """
        self._require_client()
        return [b.name for b in self._client.list_buckets()]

    # ── Object Operations ───────────────────────────────────────────

    def upload(
        self,
        local_path: str,
        gcs_path: str,
        content_type: Optional[str] = None,
        metadata: Optional[Dict[str, str]] = None,
        public: bool = False,
    ) -> Dict[str, Any]:
        """
        Upload a local file to GCS.

        Args:
            local_path:   Local file to upload
            gcs_path:     Destination blob name in bucket
            content_type: MIME type (auto-detected if None)
            metadata:     Custom metadata key-value pairs
            public:       Make object publicly readable

        Returns:
            Dict with: blob_name, size, md5, gcs_uri, public_url

        Example:
            >>> info = gcs.upload("report.pdf", "reports/report.pdf")
            >>> print(info["gcs_uri"])
        """
        self._require_bucket()
        blob = self._bucket.blob(gcs_path)
        if metadata:
            blob.metadata = metadata

        size = os.path.getsize(local_path)
        blob.upload_from_filename(local_path, content_type=content_type)
        if public:
            blob.make_public()

        blob.reload()
        result = {
            "blob_name": gcs_path,
            "size": size,
            "md5": blob.md5_hash,
            "gcs_uri": f"gs://{self.bucket_name}/{gcs_path}",
            "public_url": blob.public_url if public else None,
        }
        log.info(f"Uploaded {local_path} → gs://{self.bucket_name}/{gcs_path} ({size} bytes)")
        return result

    def upload_string(
        self,
        content: str,
        gcs_path: str,
        content_type: str = "text/plain",
    ) -> str:
        """
        Upload a string directly to GCS without a local file.

        Returns:
            GCS URI

        Example:
            >>> gcs.upload_string('{"key": "value"}', "config/settings.json", "application/json")
        """
        self._require_bucket()
        blob = self._bucket.blob(gcs_path)
        blob.upload_from_string(content, content_type=content_type)
        uri = f"gs://{self.bucket_name}/{gcs_path}"
        log.info(f"Uploaded string → {uri}")
        return uri

    def download(self, gcs_path: str, local_path: str) -> Dict[str, Any]:
        """
        Download a GCS object to a local file.

        Args:
            gcs_path:   Blob name in bucket
            local_path: Local destination path

        Returns:
            Dict with: local_path, size, md5

        Example:
            >>> gcs.download("reports/report.pdf", "/tmp/report.pdf")
        """
        self._require_bucket()
        os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
        blob = self._bucket.blob(gcs_path)
        blob.download_to_filename(local_path)
        size = os.path.getsize(local_path)
        log.info(f"Downloaded gs://{self.bucket_name}/{gcs_path} → {local_path} ({size} bytes)")
        return {"local_path": local_path, "size": size, "md5": blob.md5_hash}

    def download_as_string(self, gcs_path: str) -> str:
        """
        Download a GCS object directly as a string.

        Example:
            >>> content = gcs.download_as_string("config/settings.json")
        """
        self._require_bucket()
        blob = self._bucket.blob(gcs_path)
        return blob.download_as_text()

    def delete(self, gcs_path: str) -> bool:
        """
        Delete an object from GCS.

        Returns:
            True if deleted, False if not found

        Example:
            >>> gcs.delete("old/report.pdf")
        """
        self._require_bucket()
        try:
            self._bucket.blob(gcs_path).delete()
            log.info(f"Deleted gs://{self.bucket_name}/{gcs_path}")
            return True
        except Exception as e:
            log.warning(f"Delete failed for {gcs_path}: {e}")
            return False

    def exists(self, gcs_path: str) -> bool:
        """
        Check if a GCS object exists.

        Example:
            >>> if gcs.exists("reports/report.pdf"): print("Found!")
        """
        self._require_bucket()
        return self._bucket.blob(gcs_path).exists()

    def list_objects(
        self,
        prefix: Optional[str] = None,
        delimiter: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """
        List objects in the bucket.

        Args:
            prefix:    Filter by path prefix
            delimiter: Use '/' for directory-like listing

        Returns:
            List of dicts with: name, size, updated, content_type

        Example:
            >>> objects = gcs.list_objects(prefix="reports/2024/")
        """
        self._require_bucket()
        blobs = self._client.list_blobs(
            self.bucket_name, prefix=prefix, delimiter=delimiter
        )
        results = []
        for b in blobs:
            results.append({
                "name": b.name,
                "size": b.size,
                "updated": str(b.updated),
                "content_type": b.content_type,
            })
        return results

    def signed_url(
        self,
        gcs_path: str,
        expiry_minutes: int = 60,
        method: str = "GET",
    ) -> str:
        """
        Generate a signed URL for temporary access.

        Args:
            gcs_path:       Blob name
            expiry_minutes: URL validity duration (default 60 min)
            method:         HTTP method (GET | PUT)

        Returns:
            Signed URL string

        Example:
            >>> url = gcs.signed_url("private/data.zip", expiry_minutes=30)
        """
        self._require_bucket()
        import datetime
        blob = self._bucket.blob(gcs_path)
        url = blob.generate_signed_url(
            expiration=datetime.timedelta(minutes=expiry_minutes),
            method=method,
            version="v4",
        )
        log.info(f"Generated signed URL for {gcs_path} (expires in {expiry_minutes}m)")
        return url

    def copy(self, src_path: str, dst_path: str, dst_bucket: Optional[str] = None) -> str:
        """
        Copy an object within or across buckets.

        Returns:
            Destination GCS URI

        Example:
            >>> gcs.copy("reports/v1.pdf", "archive/reports/v1.pdf")
        """
        self._require_bucket()
        src_blob = self._bucket.blob(src_path)
        dst_bucket_obj = self._client.bucket(dst_bucket) if dst_bucket else self._bucket
        self._bucket.copy_blob(src_blob, dst_bucket_obj, dst_path)
        dst_uri = f"gs://{dst_bucket or self.bucket_name}/{dst_path}"
        log.info(f"Copied gs://{self.bucket_name}/{src_path} → {dst_uri}")
        return dst_uri

    def get_metadata(self, gcs_path: str) -> Dict[str, Any]:
        """
        Get object metadata.

        Returns:
            Dict with blob metadata

        Example:
            >>> meta = gcs.get_metadata("reports/report.pdf")
        """
        self._require_bucket()
        blob = self._bucket.blob(gcs_path)
        blob.reload()
        return {
            "name": blob.name,
            "bucket": blob.bucket.name,
            "size": blob.size,
            "content_type": blob.content_type,
            "md5": blob.md5_hash,
            "crc32c": blob.crc32c,
            "created": str(blob.time_created),
            "updated": str(blob.updated),
            "custom_metadata": blob.metadata,
        }

    def set_metadata(self, gcs_path: str, metadata: Dict[str, str]) -> None:
        """
        Set custom metadata on an object.

        Example:
            >>> gcs.set_metadata("reports/report.pdf", {"author": "omnisec", "version": "2"})
        """
        self._require_bucket()
        blob = self._bucket.blob(gcs_path)
        blob.metadata = metadata
        blob.patch()
        log.info(f"Metadata updated for {gcs_path}")

    def __repr__(self) -> str:
        return f"GCSClient(project={self.project!r}, bucket={self.bucket_name!r})"
