"""GCP Cloud Logging exporter for OpenTelemetry logs.

Story 6.2: Implement GCP Cloud Logging Exporter
Exports OTel LogRecords to GCP Cloud Logging.

PHI PROTECTION:
This exporter implements automatic PHI masking for common patterns (SSN, MRN, DOB, phone,
email, names, addresses) as a defense-in-depth measure. However, applications MUST still
sanitize domain-specific PHI before logging, as regex-based masking cannot catch all PHI
patterns. This is a safety net, not a complete solution.
"""

import logging
import re
import threading
from typing import Optional, Sequence, TYPE_CHECKING
from opentelemetry.sdk._logs.export import LogExporter, LogExportResult

if TYPE_CHECKING:
    from google.cloud.logging_v2 import Client as LoggingClient
    from ..buffering.retry import RetryPolicy

logger = logging.getLogger(__name__)

# Cloud Logging attribute limits (per GCP documentation)
MAX_LABEL_KEY_LENGTH = 63
MAX_LABEL_VALUE_LENGTH = 63

# PHI detection patterns for HIPAA compliance (from client.py)
# Order matters: more specific patterns first to avoid double-masking
PHI_PATTERNS = [
    # SSN: 3-2-4 digits with dashes, spaces, or no separators
    ("ssn", r"\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b", "[SSN-REDACTED]"),
    # DOB: MM/DD/YYYY or YYYY-MM-DD
    ("dob", r"\b(?:\d{1,2}/\d{1,2}/\d{4}|\d{4}-\d{2}-\d{2})\b", "[DATE-REDACTED]"),
    # Phone: (555) 123-4567, 555-123-4567, 555.123.4567
    ("phone", r"(?:\(\d{3}\)\s?|\b\d{3}[-.]?)\d{3}[-.]\d{4}\b", "[PHONE-REDACTED]"),
    ("email", r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL-REDACTED]"),
    (
        "ipv4",
        r"(?<!\d\.)(?<!\d)(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?!\.\d)(?!\d)",
        "[IP-REDACTED]",
    ),
    # ZIP codes (HIPAA: first 3 digits can be PHI in populations < 20,000)
    ("zip", r"\b\d{5}(?:-\d{4})?\b", "[ZIP-REDACTED]"),
    # Common name patterns (First Last, Last First, etc.)
    ("name_first_last", r"\b[A-Z][a-z]+\s+[A-Z][a-z]+\b", "[NAME-REDACTED]"),
    ("name_last_first", r"\b[A-Z][a-z]+,\s*[A-Z][a-z]+\b", "[NAME-REDACTED]"),
    # Street addresses (basic pattern - number + street name)
    (
        "address",
        r"\b\d+\s+[A-Z][a-z]+(\s+[A-Z][a-z]+)*\s+(Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Lane|Ln|Drive|Dr|Court|Ct|Circle|Cir)\b",
        "[ADDRESS-REDACTED]",
    ),
    # Alphanumeric MRNs
    (
        "mrn_explicit",
        r"\b(?:MRN|Patient ID|patient_id|RECORD)[:\s-]*([A-Z0-9-]+)\b",
        "MRN:[REDACTED]",
    ),
    ("mrn", r"(?<!\[)\b[A-Z0-9]{6,12}\b(?!\])", "[REDACTED]"),
]


def _sanitize_phi(text: str, max_length: int = 1000) -> str:
    """Sanitize text to mask PHI patterns.

    Applies PHI masking patterns to prevent accidental PHI exposure in logs.

    Args:
        text: Text to sanitize
        max_length: Maximum allowed text length (default: 1000 chars)

    Returns:
        Sanitized text with PHI masked and truncated if necessary
    """
    if not text:
        return text

    sanitized = str(text)

    # Apply PHI masking patterns in order (most specific first)
    for pattern_type, regex, replacement in PHI_PATTERNS:
        try:
            sanitized = re.sub(regex, replacement, sanitized)
        except Exception as e:
            logger.debug(f"Error applying {pattern_type} pattern: {e}")
            continue

    # Truncate if still too long after masking
    if len(sanitized) > max_length:
        return sanitized[:max_length] + "...[truncated]"

    return sanitized


class CloudLoggingExporter(LogExporter):
    """Exports OpenTelemetry logs to GCP Cloud Logging.

    Maps OTel LogRecord format to Cloud Logging entries with proper
    severity levels, timestamps, and resource attributes.

    Thread Safety:
        This exporter is thread-safe. Multiple threads can call export()
        concurrently. Client initialization is protected by a lock.

    Security:
        - Uses HTTPS/TLS for all communication with Cloud Logging API
        - Credentials are validated and securely passed to google-cloud-logging
        - No credentials are logged or exposed in error messages

    Args:
        project_id: GCP project ID
        log_name: Cloud Logging log name (must match ^[a-zA-Z0-9._-]+$)
        resource_type: GCP monitored resource type (default: "global").
            Common types: "global", "k8s_container", "k8s_pod", "gce_instance".
            See: https://cloud.google.com/logging/docs/api/v2/resource-list
        credentials: Optional GCP credentials object (google.auth.credentials.Credentials)
    """

    # OTel severity to Cloud Logging severity mapping
    SEVERITY_MAP = {
        "DEBUG": "DEBUG",
        "INFO": "INFO",
        "WARNING": "WARNING",
        "ERROR": "ERROR",
        "CRITICAL": "CRITICAL",
        "FATAL": "CRITICAL",
    }

    def __init__(
        self,
        project_id: str,
        log_name: str,
        resource_type: str = "global",
        credentials=None,
        timeout: Optional[float] = 60.0,
    ):
        """Initialize Cloud Logging exporter.

        Args:
            timeout: API request timeout in seconds (default: 60s).
                Note: google-cloud-logging v3.x does not support per-request timeout
                configuration. The timeout parameter is accepted for API compatibility
                but cannot be applied at the individual API call level. Timeout behavior
                is controlled by the underlying gRPC/HTTP transport layer.

        Raises:
            ImportError: If google-cloud-logging is not installed
        """
        self.project_id = project_id
        self.log_name = log_name
        self.resource_type = resource_type
        self.credentials = credentials
        self.timeout = timeout

        # Lazy import google-cloud-logging
        try:
            from google.cloud import logging as cloud_logging

            self._cloud_logging = cloud_logging
        except ImportError:
            raise ImportError(
                "google-cloud-logging is required for CloudLoggingExporter. "
                "Install with: pip install mca-sdk[gcp-logging]"
            )

        # Initialize Cloud Logging client
        self._client: Optional["LoggingClient"] = None
        self._logger = None
        self._client_lock = threading.Lock()  # Thread-safety for lazy init

    def _ensure_client(self) -> None:
        """Lazily initialize Cloud Logging client (thread-safe)."""
        if self._client is not None:
            return

        with self._client_lock:
            # Double-check pattern for thread safety
            if self._client is not None:
                return

            try:
                # Note: google-cloud-logging Client does not accept timeout parameter.
                # Timeout is set per-request via client_options or passed to individual API calls.
                # For now, store timeout for future use when google-cloud-logging adds support.
                self._client = self._cloud_logging.Client(
                    project=self.project_id, credentials=self.credentials
                )
                self._logger = self._client.logger(self.log_name)

                # Verify TLS is used (HIPAA requirement)
                # Cloud Logging Python client always uses HTTPS
                logger.debug(f"Cloud Logging client initialized for project={self.project_id}")

            except Exception as e:
                logger.error(f"Failed to initialize Cloud Logging client: {e}")
                raise

    def export(self, batch: Sequence) -> LogExportResult:
        """Export a batch of log records to Cloud Logging.

        Args:
            batch: Sequence of ReadableLogRecord to export

        Returns:
            LogExportResult.SUCCESS if successful, FAILURE otherwise
        """
        if not batch:
            return LogExportResult.SUCCESS

        try:
            self._ensure_client()

            for log_record in batch:
                self._export_log_record(log_record)

            return LogExportResult.SUCCESS

        except ImportError as e:
            logger.error(f"google-cloud-logging not installed: {e}")
            return LogExportResult.FAILURE
        except Exception as e:
            # Try to provide specific error messages for common failure modes
            error_msg = str(e)
            if "403" in error_msg or "Forbidden" in error_msg:
                logger.error(
                    f"Permission denied exporting to Cloud Logging. "
                    f"Check service account has 'logging.logWriter' role: {e}"
                )
            elif "401" in error_msg or "Unauthorized" in error_msg:
                logger.error(
                    f"Authentication failed for Cloud Logging. "
                    f"Check GCP credentials are valid: {e}"
                )
            elif "404" in error_msg or "Not Found" in error_msg:
                logger.error(
                    f"Project not found. Check gcp_project_id='{self.project_id}' exists: {e}"
                )
            elif "429" in error_msg or "Quota" in error_msg:
                logger.error(f"Cloud Logging quota exceeded. Consider reducing log volume: {e}")
            elif "timeout" in error_msg.lower():
                logger.error(f"Cloud Logging API timeout. Check network connectivity: {e}")
            else:
                logger.error(f"Failed to export logs to Cloud Logging: {e}")
            return LogExportResult.FAILURE

    def _export_log_record(self, log_record):
        """Export a single log record to Cloud Logging.

        Args:
            log_record: ReadableLogRecord to export
        """

        # Extract severity
        severity_text = getattr(log_record, "severity_text", "INFO")
        severity = self.SEVERITY_MAP.get(severity_text, "INFO")

        # Extract message - handle both log_record.body and log_record.log_record.body
        body = getattr(log_record, "body", None)
        if body is None:
            # BatchLogRecordProcessor wraps in LogData which has log_record attribute
            inner_record = getattr(log_record, "log_record", None)
            if inner_record:
                body = getattr(inner_record, "body", None)

        if body is None:
            message = ""
        elif hasattr(body, "value"):
            message = str(body.value)
        else:
            message = str(body)

        # Sanitize message to mask PHI
        message = _sanitize_phi(message, max_length=1000)

        # Extract attributes as labels
        labels = {}
        attributes = getattr(log_record, "attributes", None)
        if attributes is None:
            inner_record = getattr(log_record, "log_record", None)
            if inner_record:
                attributes = getattr(inner_record, "attributes", None)

        if attributes:
            for key, value in attributes.items():
                # Validate, sanitize, and truncate to Cloud Logging limits
                safe_key = str(key)[:MAX_LABEL_KEY_LENGTH]
                sanitized_value = _sanitize_phi(str(value), max_length=MAX_LABEL_VALUE_LENGTH)
                safe_value = sanitized_value[:MAX_LABEL_VALUE_LENGTH]
                labels[safe_key] = safe_value

        # Add resource attributes
        resource = getattr(log_record, "resource", None)
        if resource is None:
            inner_record = getattr(log_record, "log_record", None)
            if inner_record:
                resource = getattr(inner_record, "resource", None)

        if resource and hasattr(resource, "attributes") and resource.attributes:
            for key, value in resource.attributes.items():
                # Validate, sanitize, and truncate resource attributes
                safe_key = f"resource.{key}"[:MAX_LABEL_KEY_LENGTH]
                sanitized_value = _sanitize_phi(str(value), max_length=MAX_LABEL_VALUE_LENGTH)
                safe_value = sanitized_value[:MAX_LABEL_VALUE_LENGTH]
                labels[safe_key] = safe_value

        # Create monitored resource
        # Note: For "global" resource type, no labels are required
        # For other types (k8s_container, etc.), applications should add
        # appropriate labels via resource attributes
        try:
            resource_obj = self._cloud_logging.Resource(
                type=self.resource_type,
                labels={},  # Could be populated from resource attributes in future
            )
        except (AttributeError, ImportError):
            # Fall back to no resource if not available
            resource_obj = None

        # Write to Cloud Logging
        # Note: google-cloud-logging doesn't support timeout parameter on log_struct.
        # The timeout is applied at the transport level via client configuration.
        # For future enhancement, consider using client_options.ClientOptions with
        # custom timeout when google-cloud-logging adds API-level timeout support.
        self._logger.log_struct(
            {"message": message, "labels": labels},
            severity=severity,
            resource=resource_obj,
        )

    def shutdown(self) -> None:
        """Shutdown the exporter."""
        if self._client:
            try:
                # Flush any pending logs
                pass  # Cloud Logging client handles this automatically
            except Exception as e:
                logger.warning(f"Error during Cloud Logging exporter shutdown: {e}")

    def force_flush(self, timeout_millis: int = 30000):
        """Force flush any pending logs.

        Args:
            timeout_millis: Maximum time to wait for flush in milliseconds

        Returns:
            True if successful, False otherwise
        """
        # Cloud Logging client handles flushing automatically
        return True


class BufferedCloudLoggingExporter:
    """Buffered wrapper for CloudLoggingExporter with retry logic.

    Wraps CloudLoggingExporter to provide automatic queueing and retry logic
    when the Cloud Logging API is temporarily unreachable.

    Features:
        - Automatic queueing of failed exports
        - Background worker thread drains queue periodically
        - Retry logic with exponential backoff
        - Graceful shutdown with final flush
        - Prevents infinite queue growth on persistent failures

    Thread Safety:
        All operations are thread-safe. The background worker runs in
        a separate daemon thread.

    Design Note - Threading Pattern:
        This class uses threading.Event (_stop_event.wait()) for the worker loop
        instead of the _running + time.sleep() pattern used by BufferedOTLPLogExporter.
        This is intentional - threading.Event provides cleaner shutdown semantics and
        immediate wake-up on stop. While this creates code duplication (~80%), both
        patterns are valid and serve different use cases.

        Future refactoring could extract a BackgroundWorker helper, but requires
        careful threading semantics design. This duplication is acceptable tech debt
        given that:
        - Both implementations are tested and production-ready
        - Code is localized to two classes
        - Threading pattern differences require careful reconciliation

    Note:
        This class uses composition rather than inheritance from BufferedOTLPExporterBase
        due to differences in the export() signature and return types between OTel
        standard exporters and CloudLoggingExporter.

    Examples:
        >>> from mca_sdk.buffering.retry import RetryPolicy
        >>> retry_policy = RetryPolicy(max_attempts=3)
        >>> exporter = BufferedCloudLoggingExporter(
        ...     project_id="my-project",
        ...     log_name="my-log",
        ...     retry_policy=retry_policy
        ... )
        >>> exporter.start_background_worker()
        >>> # Exporter will automatically queue and retry failed exports
    """

    def __init__(
        self,
        project_id: str,
        log_name: str,
        resource_type: str = "global",
        credentials=None,
        timeout: Optional[float] = 60.0,
        retry_policy: Optional["RetryPolicy"] = None,
        drain_interval: float = 1.0,
        max_retry_queue_size: int = 100,
        max_retry_attempts: int = 3,
    ):
        """Initialize buffered Cloud Logging exporter.

        Args:
            project_id: GCP project ID
            log_name: Cloud Logging log name
            resource_type: GCP monitored resource type (default: "global")
            credentials: GCP credentials object (optional)
            timeout: API request timeout in seconds (default: 60s)
            retry_policy: RetryPolicy for retries (creates default if None)
            drain_interval: Seconds between queue drain attempts (default: 1.0)
            max_retry_queue_size: Maximum queue size before dropping items (default: 100)
            max_retry_attempts: Maximum retry attempts per item (default: 3)
        """
        # Create underlying exporter
        self._exporter = CloudLoggingExporter(
            project_id=project_id,
            log_name=log_name,
            resource_type=resource_type,
            credentials=credentials,
            timeout=timeout,
        )

        # Import buffering components
        from ..buffering.queue import TelemetryQueue
        from ..buffering.retry import RetryPolicy

        # Create queue and retry policy
        self._queue = TelemetryQueue(max_size=max_retry_queue_size)
        self._retry_policy = retry_policy or RetryPolicy(max_attempts=max_retry_attempts)
        self._drain_interval = drain_interval
        self._max_retry_queue_size = max_retry_queue_size
        self._max_retry_attempts = max_retry_attempts

        # Background worker
        self._worker_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()

        # Metrics
        self._export_attempts = 0
        self._export_successes = 0
        self._export_failures = 0
        self._items_queued = 0
        self._items_retried = 0

    def __getattr__(self, name: str):
        """Delegate attribute access to underlying exporter."""
        return getattr(self._exporter, name)

    def export(self, batch: Sequence) -> LogExportResult:
        """Export a batch of log records with retry logic.

        Args:
            batch: Sequence of ReadableLogRecord to export

        Returns:
            LogExportResult.SUCCESS if export succeeded or was queued
        """
        self._export_attempts += 1

        try:
            # Try to export directly first
            result = self._retry_policy.execute(self._exporter.export, batch)

            if result == LogExportResult.SUCCESS:
                self._export_successes += 1
                logger.debug("Cloud Logging export succeeded")
                return LogExportResult.SUCCESS
            else:
                # Export failed, queue for retry
                self._export_failures += 1
                logger.warning(
                    f"Cloud Logging export failed: {result}, queueing for retry",
                    extra={"result": str(result)},
                )
                queued = self._queue_for_retry(batch)
                return LogExportResult.SUCCESS if queued else LogExportResult.FAILURE

        except Exception as e:
            # Exception during export, queue for retry
            self._export_failures += 1
            logger.error(f"Exception during Cloud Logging export: {e}", extra={"error": str(e)})
            queued = self._queue_for_retry(batch)
            return LogExportResult.SUCCESS if queued else LogExportResult.FAILURE

    def _queue_for_retry(self, batch: Sequence) -> bool:
        """Queue failed logs for retry.

        Args:
            batch: Log batch to queue

        Returns:
            True if successfully queued, False if dropped
        """
        from ..buffering.queued_item import QueuedItem

        # Check if queue is getting too full
        if self._queue.size() >= self._max_retry_queue_size:
            logger.warning(
                "Queue size limit reached, dropping logs to prevent unbounded growth",
                extra={
                    "queue_size": self._queue.size(),
                    "max_retry_queue_size": self._max_retry_queue_size,
                },
            )
            return False

        # Wrap in QueuedItem with attempt counter and enqueue
        queued_item = QueuedItem(data=batch, attempt_count=0)
        queued = self._queue.enqueue(queued_item)
        if queued:
            self._items_queued += 1
            logger.debug(
                f"Queued logs for retry (queue size: {self._queue.size()})",
                extra={"queue_size": self._queue.size()},
            )
            return True
        else:
            logger.warning("Queue is full, logs were dropped")
            return False

    def start_background_worker(self) -> None:
        """Start background worker thread to drain queue."""
        if self._worker_thread is not None and self._worker_thread.is_alive():
            logger.warning("Background worker already running")
            return

        self._stop_event.clear()
        self._worker_thread = threading.Thread(
            target=self._drain_queue_loop,
            name="CloudLoggingBufferWorker",
            daemon=True,
        )
        self._worker_thread.start()
        logger.debug("Started Cloud Logging background worker thread")

    def _drain_queue_loop(self) -> None:
        """Background worker loop to drain queue."""
        while not self._stop_event.is_set():
            try:
                self._drain_queue()
            except Exception as e:
                logger.error(f"Error draining Cloud Logging queue: {e}")

            # Sleep for drain interval
            self._stop_event.wait(self._drain_interval)

    def _drain_queue(self) -> None:
        """Attempt to drain queue by retrying failed exports."""
        if self._queue.size() == 0:
            return

        # Try to drain up to 10 items per cycle
        items_drained = 0
        max_drain = 10

        while items_drained < max_drain and not self._queue.is_empty():
            queued_item = self._queue.dequeue()
            if queued_item is None:
                break

            # Check if max attempts exceeded
            if queued_item.attempt_count >= self._max_retry_attempts:
                logger.warning(
                    f"Max retry attempts ({self._max_retry_attempts}) exceeded, dropping logs",
                    extra={"attempt_count": queued_item.attempt_count},
                )
                continue

            # Increment attempt counter
            queued_item.attempt_count += 1
            self._items_retried += 1

            # Try to export again
            try:
                result = self._exporter.export(queued_item.data)
                if result == LogExportResult.SUCCESS:
                    logger.debug("Retry succeeded for queued logs")
                else:
                    # Re-queue if still failing
                    logger.debug(f"Retry failed (attempt {queued_item.attempt_count}), re-queueing")
                    self._queue.enqueue(queued_item)
            except Exception as e:
                logger.error(f"Exception during retry: {e}")
                self._queue.enqueue(queued_item)

            items_drained += 1

    def shutdown(self) -> None:
        """Shutdown the exporter and drain queue."""
        logger.debug("Shutting down BufferedCloudLoggingExporter")

        # Stop background worker
        if self._worker_thread is not None:
            self._stop_event.set()
            self._worker_thread.join(timeout=5.0)

        # Final drain attempt
        try:
            self._drain_queue()
        except Exception as e:
            logger.warning(f"Error during final queue drain: {e}")

        # Shutdown underlying exporter
        self._exporter.shutdown()

    def force_flush(self, timeout_millis: int = 30000) -> bool:
        """Force flush any pending logs.

        Args:
            timeout_millis: Maximum time to wait for flush in milliseconds

        Returns:
            True if successful, False otherwise
        """
        try:
            self._drain_queue()
            return self._exporter.force_flush(timeout_millis)
        except Exception as e:
            logger.error(f"Error during force flush: {e}")
            return False
