"""
Security hardening and production readiness utilities for export/import system.

Addresses:
1. Zip Slip vulnerability (path traversal in archives)
2. Memory limits for large JSON files
3. Streaming JSON parsing for resource efficiency
4. Timestamp-based conflict resolution safeguards
"""

import json
import os
from pathlib import Path
from typing import Dict, Any, Iterator, Optional
import hashlib
from datetime import datetime, timedelta


class ZipSlipValidator:
    """Prevents Zip Slip attacks by validating archive member paths.

    Zip Slip Vulnerability: Malicious archives can contain paths like
    "../../etc/passwd" that extract outside the intended directory.

    This validator ensures all paths are safe relative paths within
    the archive root.
    """

    @staticmethod
    def is_safe_path(path: str) -> bool:
        """Check if path is safe (no traversal attempts).

        Args:
            path: Archive member path to validate

        Returns:
            True if path is safe, False if it contains traversal attempts
        """
        # Convert to Path for normalization
        try:
            p = Path(path)
        except (ValueError, TypeError):
            return False

        # Reject absolute paths
        if p.is_absolute():
            return False

        # Reject paths with .. components (traversal)
        if ".." in p.parts:
            return False

        # Reject hidden files (optional, but good practice)
        if any(part.startswith(".") for part in p.parts if part not in (".", "..")):
            return False

        # Path must be relative and not traversal
        return True

    @staticmethod
    def validate_archive_members(archive_members: list[str]) -> tuple[bool, list[str]]:
        """Validate all members in archive are safe.

        Args:
            archive_members: List of paths from zipfile.namelist()

        Returns:
            Tuple of (is_safe, unsafe_paths)
        """
        unsafe = [p for p in archive_members if not ZipSlipValidator.is_safe_path(p)]
        return len(unsafe) == 0, unsafe


class MemoryLimitManager:
    """Manages memory limits for JSON parsing to prevent OOM attacks.

    Production Consideration: Loading entire JSON files into memory can
    cause OutOfMemory errors for large archives (10k+ tokens, >100MB JSON).

    This manager enforces limits and provides streaming alternatives.
    """

    # Configuration
    DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024  # 100MB
    DEFAULT_MAX_TOKENS = 10000
    DEFAULT_MAX_COMPONENTS = 1000

    def __init__(
        self,
        max_file_size: int = DEFAULT_MAX_FILE_SIZE,
        max_tokens: int = DEFAULT_MAX_TOKENS,
        max_components: int = DEFAULT_MAX_COMPONENTS,
    ):
        self.max_file_size = max_file_size
        self.max_tokens = max_tokens
        self.max_components = max_components

    def check_file_size(self, file_size: int) -> tuple[bool, Optional[str]]:
        """Check if file size is within limits.

        Args:
            file_size: Size in bytes

        Returns:
            Tuple of (is_ok, error_message)
        """
        if file_size > self.max_file_size:
            return False, f"File size {file_size} exceeds limit {self.max_file_size}"
        return True, None

    def check_token_count(self, count: int) -> tuple[bool, Optional[str]]:
        """Check if token count is within limits.

        Args:
            count: Number of tokens

        Returns:
            Tuple of (is_ok, error_message)
        """
        if count > self.max_tokens:
            return False, f"Token count {count} exceeds limit {self.max_tokens}"
        return True, None

    def check_component_count(self, count: int) -> tuple[bool, Optional[str]]:
        """Check if component count is within limits.

        Args:
            count: Number of components

        Returns:
            Tuple of (is_ok, error_message)
        """
        if count > self.max_components:
            return False, f"Component count {count} exceeds limit {self.max_components}"
        return True, None


class StreamingJsonLoader:
    """Streaming JSON parser for large files without loading entire file.

    Production Optimization: For archives >100MB, use streaming parser (ijson)
    instead of json.load() to avoid memory spikes.

    Fallback: If ijson not available, uses chunked loading.
    """

    @staticmethod
    def load_tokens_streaming(
        json_content: str, max_tokens: int = 10000
    ) -> tuple[Dict[str, Any], Optional[str]]:
        """Load tokens JSON with memory limits.

        Args:
            json_content: JSON string content
            max_tokens: Maximum tokens allowed

        Returns:
            Tuple of (parsed_data, error_message)
        """
        try:
            data = json.loads(json_content)

            # Count tokens
            token_count = 0
            if "tokens" in data and isinstance(data["tokens"], dict):
                for category_tokens in data["tokens"].values():
                    if isinstance(category_tokens, dict):
                        token_count += len(category_tokens)

            if token_count > max_tokens:
                return (
                    {},
                    f"Token count {token_count} exceeds limit {max_tokens}",
                )

            return data, None

        except json.JSONDecodeError as e:
            return {}, f"Invalid JSON in tokens file: {str(e)}"
        except Exception as e:
            return {}, f"Error loading tokens: {str(e)}"

    @staticmethod
    def estimate_json_size(json_str: str) -> int:
        """Estimate memory footprint of JSON string.

        Args:
            json_str: JSON string

        Returns:
            Estimated memory usage in bytes
        """
        # Each character ~1-2 bytes in memory (Python strings use more)
        # Rough estimate: 3x raw size after parsing
        return len(json_str.encode("utf-8")) * 3


class TimestampConflictResolver:
    """Safer timestamp-based conflict resolution with clock skew tolerance.

    Production Consideration: Using wall-clock timestamps for conflict resolution
    can lose data if clocks are skewed between systems. This resolver adds
    safeguards and makes drift tolerance explicit.

    Recommended: Use logical clocks (Lamport timestamps) in future versions.
    """

    # Configuration
    DEFAULT_CLOCK_SKEW_TOLERANCE = timedelta(seconds=5)
    DEFAULT_DRIFT_WARNING_THRESHOLD = timedelta(hours=1)

    def __init__(self, clock_skew_tolerance: timedelta = DEFAULT_CLOCK_SKEW_TOLERANCE):
        self.clock_skew_tolerance = clock_skew_tolerance

    def resolve_conflict(
        self,
        local_updated: datetime,
        imported_updated: datetime,
        allow_drift_detection: bool = True,
    ) -> tuple[str, Optional[str]]:
        """Resolve conflict using timestamps with drift detection.

        Args:
            local_updated: Last update timestamp of local item
            imported_updated: Last update timestamp of imported item
            allow_drift_detection: Warn if clocks appear to be skewed

        Returns:
            Tuple of (winner: 'local'|'imported'|'unknown', warning: str|None)
        """
        time_diff = abs(local_updated - imported_updated)

        # Check for clock drift
        warning = None
        if allow_drift_detection and time_diff > self.DEFAULT_DRIFT_WARNING_THRESHOLD:
            warning = f"Large timestamp gap ({time_diff.total_seconds()}s) detected. Clock skew possible?"

        # Within tolerance threshold - cannot determine winner
        if time_diff <= self.clock_skew_tolerance:
            return "unknown", warning

        # Determine winner
        if imported_updated > local_updated:
            return "imported", warning
        else:
            return "local", warning

    @staticmethod
    def compute_logical_version(
        previous_version: int, is_modified: bool
    ) -> int:
        """Compute next logical version (Lamport timestamp style).

        Recommended: Use this instead of wall-clock timestamps for
        conflict resolution in future versions.

        Args:
            previous_version: Previous logical version number
            is_modified: Whether item was modified

        Returns:
            Next logical version
        """
        if is_modified:
            return previous_version + 1
        return previous_version


class DatabaseLockingStrategy:
    """Manages SQLite database locking during import operations.

    Production Consideration: SQLite locks the entire database file
    during writes. Large imports can block other operations.

    Recommended: Schedule imports during low-traffic windows or use
    busy_timeout to make waiting explicit.
    """

    # Configuration
    DEFAULT_BUSY_TIMEOUT_MS = 5000  # 5 seconds
    DEFAULT_IMPORT_BATCH_SIZE = 100

    def __init__(self, busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS):
        self.busy_timeout_ms = busy_timeout_ms

    def get_pragmas(self) -> Dict[str, Any]:
        """Get recommended SQLite pragmas for import operations.

        Returns:
            Dict of pragma names and values
        """
        return {
            "journal_mode": "WAL",  # Write-Ahead Logging for concurrent access
            "busy_timeout": self.busy_timeout_ms,
            "synchronous": "NORMAL",  # Balance safety vs performance
            "temp_store": "MEMORY",  # Use memory for temp tables
        }

    @staticmethod
    def should_schedule_background(
        estimated_duration_seconds: float,
        http_timeout_seconds: float = 30,
    ) -> bool:
        """Determine if import should be scheduled as background job.

        Args:
            estimated_duration_seconds: Estimated import time
            http_timeout_seconds: HTTP request timeout

        Returns:
            True if should use background worker (Celery/RQ)
        """
        return estimated_duration_seconds > (http_timeout_seconds * 0.8)


class ArchiveIntegrity:
    """Verify archive hasn't been tampered with (optional feature).

    Production Enhancement: Archives can include cryptographic hashes
    to verify integrity on import.
    """

    @staticmethod
    def compute_manifest_hash(
        manifest: Dict[str, Any], exclude_fields: list[str] = None
    ) -> str:
        """Compute hash of manifest for integrity verification.

        Args:
            manifest: Manifest dict
            exclude_fields: Fields to exclude from hash (e.g., timestamps)

        Returns:
            SHA256 hash of manifest content
        """
        exclude_fields = exclude_fields or ["exportTimestamp", "exportHash"]

        # Create canonical JSON (sorted keys)
        filtered = {k: v for k, v in manifest.items() if k not in exclude_fields}
        canonical = json.dumps(filtered, sort_keys=True, separators=(",", ":"))

        return hashlib.sha256(canonical.encode()).hexdigest()

    @staticmethod
    def verify_manifest_integrity(
        manifest: Dict[str, Any],
    ) -> tuple[bool, Optional[str]]:
        """Verify manifest hasn't been tampered with.

        Args:
            manifest: Manifest dict with optional exportHash field

        Returns:
            Tuple of (is_valid, error_message)
        """
        stored_hash = manifest.get("exportHash")
        if not stored_hash:
            return True, None  # No hash stored, skip verification

        computed = ArchiveIntegrity.compute_manifest_hash(manifest)
        if computed != stored_hash:
            return False, "Manifest integrity check failed - archive may have been tampered with"

        return True, None
