#!/usr/bin/env python3
"""
Database Persistence Pattern Comparison

Compares WeaselDB's batched S3 persistence approach against traditional
database persistence patterns to understand trade-offs in latency,
throughput, consistency, and operational complexity.

Simulated approaches:
1. WeaselDB: Batched async S3 persistence with optimistic concurrency
2. Traditional WAL: Write-ahead log with periodic sync to disk
3. Synchronous: Immediate disk sync per transaction (like PostgreSQL serializable)
4. Group Commit: Batched disk writes with configurable group size
5. Async Replication: Immediate response with async durability
"""

import heapq
import random
import statistics
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Tuple, Any
import numpy as np
import matplotlib.pyplot as plt
from collections import defaultdict, deque
from abc import ABC, abstractmethod
from enum import Enum
import time


class PersistencePattern(Enum):
    WEASELDB_S3 = "WeaselDB S3 Batched"
    TRADITIONAL_WAL = "Traditional WAL"
    SYNCHRONOUS = "Synchronous Disk"
    GROUP_COMMIT = "Group Commit"
    ASYNC_REPLICATION = "Async Replication"


@dataclass
class Transaction:
    """Represents a database transaction/commit"""

    txn_id: int
    arrival_time: float
    size_bytes: int = 1024
    requires_durability: bool = True  # Some txns can be async


@dataclass
class PersistenceMetrics:
    """Metrics for a persistence approach"""

    pattern: PersistencePattern
    total_transactions: int
    completed_transactions: int

    # Latency metrics (milliseconds)
    min_latency: float
    mean_latency: float
    median_latency: float
    p95_latency: float
    p99_latency: float
    max_latency: float

    # Throughput metrics
    avg_throughput_tps: float
    peak_throughput_tps: float

    # Resource metrics
    avg_disk_iops: float
    avg_network_mbps: float
    storage_efficiency: float  # GB stored / GB logical data

    # Consistency metrics
    durability_guarantee: str
    consistency_model: str
    recovery_time_estimate: float

    # Operational metrics
    operational_complexity: int  # 1-10 scale
    infrastructure_cost: float  # Relative cost per transaction


class PersistenceSimulator(ABC):
    """Abstract base class for persistence pattern simulators"""

    def __init__(
        self, simulation_duration: float = 60.0, arrival_rate_per_sec: float = 1000.0
    ):
        self.simulation_duration = simulation_duration
        self.arrival_rate_per_sec = arrival_rate_per_sec

        # Simulation state
        self.current_time = 0.0
        self.event_queue = []
        self.completed_transactions = []
        self.next_txn_id = 0

        # Metrics tracking
        self.disk_iops = []
        self.network_usage = []
        self.throughput_samples = []

        # Random number generator
        self.rng = np.random.RandomState(42)

    def generate_transaction(self, arrival_time: float) -> Transaction:
        """Generate a transaction with realistic characteristics"""
        # Size distribution: mostly small, some large
        if self.rng.random() < 0.8:  # 80% small transactions
            size = self.rng.randint(100, 2048)  # 100B - 2KB
        elif self.rng.random() < 0.95:  # 15% medium
            size = self.rng.randint(2048, 20480)  # 2KB - 20KB
        else:  # 5% large transactions
            size = self.rng.randint(20480, 102400)  # 20KB - 100KB

        return Transaction(
            txn_id=self.next_txn_id,
            arrival_time=arrival_time,
            size_bytes=size,
            requires_durability=True,
        )

    @abstractmethod
    def process_transaction(self, txn: Transaction) -> None:
        """Process a transaction according to the persistence pattern"""
        pass

    @abstractmethod
    def get_pattern_name(self) -> PersistencePattern:
        """Return the persistence pattern this simulator implements"""
        pass

    def run_simulation(self) -> PersistenceMetrics:
        """Run the simulation and return metrics"""
        # Generate arrival events
        self._generate_arrivals()

        # Process events
        while self.event_queue and self.current_time < self.simulation_duration:
            time, event_type, data = heapq.heappop(self.event_queue)
            self.current_time = time

            if event_type == "transaction_arrival":
                self.process_transaction(data)
            elif event_type == "custom":
                self._handle_custom_event(data)

        return self._calculate_metrics()

    def _generate_arrivals(self):
        """Generate Poisson arrival events"""
        time = 0.0
        while time < self.simulation_duration:
            inter_arrival = self.rng.exponential(1.0 / self.arrival_rate_per_sec)
            time += inter_arrival

            if time >= self.simulation_duration:
                break

            txn = self.generate_transaction(time)
            self.next_txn_id += 1

            heapq.heappush(self.event_queue, (time, "transaction_arrival", txn))

    def _handle_custom_event(self, data):
        """Handle custom events - override in subclasses"""
        pass

    def schedule_event(self, time: float, event_type: str, data: Any):
        """Schedule a custom event"""
        heapq.heappush(self.event_queue, (time, event_type, data))

    def _calculate_metrics(self) -> PersistenceMetrics:
        """Calculate performance metrics from completed transactions"""
        if not self.completed_transactions:
            raise ValueError("No transactions completed")

        latencies = [txn["latency_ms"] for txn in self.completed_transactions]

        return PersistenceMetrics(
            pattern=self.get_pattern_name(),
            total_transactions=self.next_txn_id,
            completed_transactions=len(self.completed_transactions),
            # Latency metrics
            min_latency=min(latencies),
            mean_latency=statistics.mean(latencies),
            median_latency=statistics.median(latencies),
            p95_latency=np.percentile(latencies, 95),
            p99_latency=np.percentile(latencies, 99),
            max_latency=max(latencies),
            # Throughput metrics
            avg_throughput_tps=len(self.completed_transactions)
            / self.simulation_duration,
            peak_throughput_tps=self._calculate_peak_throughput(),
            # Resource metrics - implemented by subclasses
            avg_disk_iops=statistics.mean(self.disk_iops) if self.disk_iops else 0,
            avg_network_mbps=(
                statistics.mean(self.network_usage) if self.network_usage else 0
            ),
            storage_efficiency=self._calculate_storage_efficiency(),
            # Pattern-specific characteristics
            durability_guarantee=self._get_durability_guarantee(),
            consistency_model=self._get_consistency_model(),
            recovery_time_estimate=self._get_recovery_time(),
            operational_complexity=self._get_operational_complexity(),
            infrastructure_cost=self._get_infrastructure_cost(),
        )

    def _calculate_peak_throughput(self) -> float:
        """Calculate peak throughput in 1-second windows"""
        if not self.completed_transactions:
            return 0.0

        # Group transactions by second
        throughput_by_second = defaultdict(int)
        for txn in self.completed_transactions:
            second = int(txn["completion_time"])
            throughput_by_second[second] += 1

        return max(throughput_by_second.values()) if throughput_by_second else 0

    # Abstract methods for pattern-specific characteristics
    @abstractmethod
    def _calculate_storage_efficiency(self) -> float:
        pass

    @abstractmethod
    def _get_durability_guarantee(self) -> str:
        pass

    @abstractmethod
    def _get_consistency_model(self) -> str:
        pass

    @abstractmethod
    def _get_recovery_time(self) -> float:
        pass

    @abstractmethod
    def _get_operational_complexity(self) -> int:
        pass

    @abstractmethod
    def _get_infrastructure_cost(self) -> float:
        pass


class WeaselDBSimulator(PersistenceSimulator):
    """WeaselDB's batched S3 persistence simulation"""

    def __init__(
        self,
        batch_timeout_ms: float = 1.0,
        batch_size_threshold: int = 800000,
        max_in_flight: int = 50,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.batch_timeout_ms = batch_timeout_ms
        self.batch_size_threshold = batch_size_threshold
        self.max_in_flight = max_in_flight

        # State
        self.current_batch = []
        self.batch_start_time = None
        self.in_flight_batches = {}
        self.next_batch_id = 0

        # S3 characteristics
        self.s3_base_latency_ms = 60.0  # From our simulation
        self.s3_size_penalty_per_mb = 20.0

    def get_pattern_name(self) -> PersistencePattern:
        return PersistencePattern.WEASELDB_S3

    def process_transaction(self, txn: Transaction):
        """Process transaction using WeaselDB batching logic"""
        # Add to current batch
        if not self.current_batch:
            self.batch_start_time = self.current_time

        self.current_batch.append(txn)

        # Check if we should send batch
        if (
            self._should_send_batch()
            and len(self.in_flight_batches) < self.max_in_flight
        ):
            self._send_current_batch()

    def _should_send_batch(self) -> bool:
        """Check batch triggers"""
        if not self.current_batch:
            return False

        # Size trigger
        batch_size = sum(txn.size_bytes for txn in self.current_batch)
        if batch_size >= self.batch_size_threshold:
            return True

        # Time trigger
        if self.batch_start_time and (self.current_time - self.batch_start_time) >= (
            self.batch_timeout_ms / 1000.0
        ):
            return True

        return False

    def _send_current_batch(self):
        """Send current batch to S3"""
        if not self.current_batch:
            return

        batch_id = self.next_batch_id
        self.next_batch_id += 1

        batch_size = sum(txn.size_bytes for txn in self.current_batch)

        # Sample S3 latency
        s3_latency = self._sample_s3_latency(batch_size) / 1000.0
        completion_time = self.current_time + s3_latency

        # Track batch
        self.in_flight_batches[batch_id] = {
            "transactions": self.current_batch.copy(),
            "sent_time": self.current_time,
            "completion_time": completion_time,
            "size_bytes": batch_size,
        }

        # Schedule completion
        self.schedule_event(
            completion_time, "custom", {"type": "batch_complete", "batch_id": batch_id}
        )

        # Track network usage
        self.network_usage.append(batch_size / (1024 * 1024))  # MB

        # Clear batch
        self.current_batch.clear()
        self.batch_start_time = None

    def _sample_s3_latency(self, batch_size_bytes: int) -> float:
        """Sample S3 latency with size scaling"""
        base = self.s3_base_latency_ms
        size_penalty = (batch_size_bytes / (1024 * 1024)) * self.s3_size_penalty_per_mb
        variable = self.rng.gamma(2.0, 15.0)  # Variable component
        return 30.0 + variable + size_penalty  # 30ms RTT + variable + size

    def _handle_custom_event(self, data):
        """Handle batch completion events"""
        if data["type"] == "batch_complete":
            batch_id = data["batch_id"]
            if batch_id in self.in_flight_batches:
                batch = self.in_flight_batches.pop(batch_id)

                # Mark transactions complete
                for txn in batch["transactions"]:
                    latency_ms = (self.current_time - txn.arrival_time) * 1000
                    self.completed_transactions.append(
                        {
                            "txn_id": txn.txn_id,
                            "arrival_time": txn.arrival_time,
                            "completion_time": self.current_time,
                            "latency_ms": latency_ms,
                            "size_bytes": txn.size_bytes,
                        }
                    )

    def _calculate_storage_efficiency(self) -> float:
        return 1.0  # S3 has no storage overhead

    def _get_durability_guarantee(self) -> str:
        return "Eventually durable (S3 11 9's)"

    def _get_consistency_model(self) -> str:
        return "Optimistic concurrency, eventual consistency"

    def _get_recovery_time(self) -> float:
        return 0.1  # Fast recovery, just reconnect to S3

    def _get_operational_complexity(self) -> int:
        return 3  # Moderate - S3 managed, but need batching logic

    def _get_infrastructure_cost(self) -> float:
        return 1.0  # Baseline cost


class TraditionalWALSimulator(PersistenceSimulator):
    """Traditional Write-Ahead Log with periodic sync"""

    def __init__(
        self,
        wal_sync_interval_ms: float = 10.0,
        checkpoint_interval_sec: float = 30.0,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.wal_sync_interval_ms = wal_sync_interval_ms
        self.checkpoint_interval_sec = checkpoint_interval_sec

        # WAL state
        self.wal_buffer = []
        self.last_sync_time = 0.0
        self.pending_transactions = {}  # Waiting for sync

        # EBS characteristics
        self.disk_latency_ms = 1.0  # EBS base latency
        self.disk_iops_limit = 10000  # Typical SSD

        # Schedule periodic syncs
        self._schedule_periodic_syncs()

    def get_pattern_name(self) -> PersistencePattern:
        return PersistencePattern.TRADITIONAL_WAL

    def process_transaction(self, txn: Transaction):
        """Write to WAL buffer, wait for sync"""
        # Write to WAL buffer (immediate)
        self.wal_buffer.append(txn)
        self.pending_transactions[txn.txn_id] = txn

        # Track disk IOPS (write to WAL)
        self.disk_iops.append(1.0)

    def _schedule_periodic_syncs(self):
        """Schedule periodic WAL sync events"""
        sync_time = self.wal_sync_interval_ms / 1000.0
        while sync_time < self.simulation_duration:
            self.schedule_event(sync_time, "custom", {"type": "wal_sync"})
            sync_time += self.wal_sync_interval_ms / 1000.0

    def _handle_custom_event(self, data):
        """Handle WAL sync events"""
        if data["type"] == "wal_sync":
            self._perform_wal_sync()

    def _perform_wal_sync(self):
        """Sync WAL buffer to disk"""
        if not self.wal_buffer:
            return

        # Calculate sync latency based on buffer size
        buffer_size = sum(txn.size_bytes for txn in self.wal_buffer)
        sync_latency = self._calculate_disk_sync_latency(buffer_size)

        completion_time = self.current_time + sync_latency / 1000.0

        # Schedule sync completion
        syncing_txns = list(self.wal_buffer)
        self.schedule_event(
            completion_time,
            "custom",
            {"type": "sync_complete", "transactions": syncing_txns},
        )

        # Track IOPS for sync operation
        self.disk_iops.append(len(self.wal_buffer))

        # Clear buffer
        self.wal_buffer.clear()
        self.last_sync_time = self.current_time

    def _calculate_disk_sync_latency(self, size_bytes: int) -> float:
        """Calculate disk sync latency with realistic fsync modeling including directory sync"""
        # Data write to page cache
        write_latency = 0.1  # Fast write to page cache

        # EBS sequential write throughput
        throughput_mbps = 1000.0  # EBS gp3 throughput
        size_mb = size_bytes / (1024 * 1024)
        transfer_latency = size_mb * (1000.0 / throughput_mbps)

        # WAL file fsync latency on EBS
        # fdatasync on EBS with network replication
        file_fsync_base = 0.8  # Higher base latency on EBS
        file_fsync_variable = self.rng.gamma(2.2, 0.4)  # More variable due to network

        # Size penalty for large WAL syncs
        size_penalty = min(size_mb * 0.05, 1.0)  # Smaller penalty than batched writes

        file_fsync_latency = file_fsync_base + file_fsync_variable + size_penalty

        # WAL typically appends to existing files, so no directory fsync needed
        # Directory fsync only required when creating new WAL segments
        return write_latency + transfer_latency + file_fsync_latency

    def _handle_custom_event(self, data):
        """Handle sync completion"""
        if data["type"] == "wal_sync":
            self._perform_wal_sync()
        elif data["type"] == "sync_complete":
            # Mark transactions as durable
            for txn in data["transactions"]:
                if txn.txn_id in self.pending_transactions:
                    del self.pending_transactions[txn.txn_id]

                    latency_ms = (self.current_time - txn.arrival_time) * 1000
                    self.completed_transactions.append(
                        {
                            "txn_id": txn.txn_id,
                            "arrival_time": txn.arrival_time,
                            "completion_time": self.current_time,
                            "latency_ms": latency_ms,
                            "size_bytes": txn.size_bytes,
                        }
                    )

    def _calculate_storage_efficiency(self) -> float:
        return 2.0  # WAL + main storage

    def _get_durability_guarantee(self) -> str:
        return "ACID durable after WAL sync"

    def _get_consistency_model(self) -> str:
        return "Strict ACID consistency"

    def _get_recovery_time(self) -> float:
        return 30.0  # WAL replay time

    def _get_operational_complexity(self) -> int:
        return 7  # Complex - WAL management, checkpoints, recovery

    def _get_infrastructure_cost(self) -> float:
        return 1.5  # Higher due to disk I/O overhead


class SynchronousSimulator(PersistenceSimulator):
    """Synchronous disk persistence (like PostgreSQL with synchronous_commit=on)"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.disk_latency_ms = 1.0  # EBS base latency

    def get_pattern_name(self) -> PersistencePattern:
        return PersistencePattern.SYNCHRONOUS

    def process_transaction(self, txn: Transaction):
        """Immediately sync transaction to disk"""
        # Calculate disk write latency
        disk_latency = self._calculate_disk_latency(txn.size_bytes) / 1000.0
        completion_time = self.current_time + disk_latency

        # Schedule completion
        self.schedule_event(
            completion_time,
            "custom",
            {"type": "disk_write_complete", "transaction": txn},
        )

        # Track disk IOPS
        self.disk_iops.append(1.0)

    def _calculate_disk_latency(self, size_bytes: int) -> float:
        """Calculate per-transaction disk write latency with fsync"""
        # Write to page cache (fast)
        write_latency = 0.1

        # FSYNC latency for immediate durability on EBS
        # Each transaction requires its own fsync - expensive on network storage!
        fsync_base = 1.5  # Much higher base latency for individual fsyncs on EBS
        fsync_variable = self.rng.gamma(2.3, 0.5)  # More variable due to network

        # Small write penalty - less efficient than batched writes
        if size_bytes < 4096:
            penalty = 0.5  # Individual small writes are less efficient
        else:
            penalty = 0.0

        fsync_latency = fsync_base + fsync_variable + penalty

        # Synchronous commits to existing database files don't need directory fsync
        # Directory fsync only needed when creating new database files/tablespaces
        return write_latency + fsync_latency

    def _handle_custom_event(self, data):
        """Handle disk write completion"""
        if data["type"] == "disk_write_complete":
            txn = data["transaction"]
            latency_ms = (self.current_time - txn.arrival_time) * 1000

            self.completed_transactions.append(
                {
                    "txn_id": txn.txn_id,
                    "arrival_time": txn.arrival_time,
                    "completion_time": self.current_time,
                    "latency_ms": latency_ms,
                    "size_bytes": txn.size_bytes,
                }
            )

    def _calculate_storage_efficiency(self) -> float:
        return 1.5  # Some overhead for metadata

    def _get_durability_guarantee(self) -> str:
        return "Immediate ACID durability"

    def _get_consistency_model(self) -> str:
        return "Strict ACID with immediate consistency"

    def _get_recovery_time(self) -> float:
        return 5.0  # Fast recovery, data already on disk

    def _get_operational_complexity(self) -> int:
        return 5  # Moderate - standard database operations

    def _get_infrastructure_cost(self) -> float:
        return 2.0  # High due to disk I/O per transaction


class WeaselDBDiskSimulator(PersistenceSimulator):
    """WeaselDB's batched disk persistence simulation"""

    def __init__(
        self,
        batch_timeout_ms: float = 1.0,
        batch_size_threshold: int = 800000,
        max_in_flight: int = 50,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.batch_timeout_ms = batch_timeout_ms
        self.batch_size_threshold = batch_size_threshold
        self.max_in_flight = max_in_flight

        # State
        self.current_batch = []
        self.batch_start_time = None
        self.in_flight_batches = {}
        self.next_batch_id = 0

        # EBS characteristics (gp3 or io2 volumes)
        self.disk_base_latency_ms = 0.5  # EBS has higher base latency than local NVMe
        self.disk_throughput_mbps = 1000.0  # EBS gp3 max throughput

    def get_pattern_name(self) -> PersistencePattern:
        return PersistencePattern.WEASELDB_S3  # Reuse enum, but it's actually disk

    def process_transaction(self, txn: Transaction):
        """Process transaction using WeaselDB batching logic"""
        # Add to current batch
        if not self.current_batch:
            self.batch_start_time = self.current_time

        self.current_batch.append(txn)

        # Check if we should send batch
        if (
            self._should_send_batch()
            and len(self.in_flight_batches) < self.max_in_flight
        ):
            self._send_current_batch()

    def _should_send_batch(self) -> bool:
        """Check batch triggers"""
        if not self.current_batch:
            return False

        # Size trigger
        batch_size = sum(txn.size_bytes for txn in self.current_batch)
        if batch_size >= self.batch_size_threshold:
            return True

        # Time trigger
        if self.batch_start_time and (self.current_time - self.batch_start_time) >= (
            self.batch_timeout_ms / 1000.0
        ):
            return True

        return False

    def _send_current_batch(self):
        """Send current batch to disk"""
        if not self.current_batch:
            return

        batch_id = self.next_batch_id
        self.next_batch_id += 1

        batch_size = sum(txn.size_bytes for txn in self.current_batch)

        # Sample disk write latency
        disk_latency = self._sample_disk_latency(batch_size) / 1000.0
        completion_time = self.current_time + disk_latency

        # Track batch
        self.in_flight_batches[batch_id] = {
            "transactions": self.current_batch.copy(),
            "sent_time": self.current_time,
            "completion_time": completion_time,
            "size_bytes": batch_size,
        }

        # Schedule completion
        self.schedule_event(
            completion_time, "custom", {"type": "batch_complete", "batch_id": batch_id}
        )

        # Track disk IOPS (one write operation per batch)
        self.disk_iops.append(1.0)

        # Clear batch
        self.current_batch.clear()
        self.batch_start_time = None

    def _sample_disk_latency(self, batch_size_bytes: int) -> float:
        """Sample disk write latency with realistic fsync modeling including directory sync"""
        # Base latency for the write command (data goes to page cache)
        write_latency = self.disk_base_latency_ms

        # Throughput-based latency for data transfer to page cache
        size_mb = batch_size_bytes / (1024 * 1024)
        transfer_latency = (
            size_mb / self.disk_throughput_mbps
        ) * 1000.0  # Convert to ms

        # FSYNC latency for EBS - forces write to replicated storage
        # EBS has higher fsync latency due to network replication
        file_fsync_base = 1.0  # Higher base fsync latency for EBS
        file_fsync_variable = self.rng.gamma(2.5, 0.6)  # More variable due to network

        # Size-dependent fsync penalty for large batches
        size_penalty = min(size_mb * 0.1, 2.0)  # Max 2ms penalty

        file_fsync_latency = file_fsync_base + file_fsync_variable + size_penalty

        # Directory fsync latency - required for WeaselDB batch file creation on EBS
        # EBS directory metadata sync is also network-replicated
        dir_fsync_base = 0.5  # Higher directory metadata sync latency on EBS
        dir_fsync_variable = self.rng.gamma(1.8, 0.3)  # More variable due to network

        dir_fsync_latency = dir_fsync_base + dir_fsync_variable

        # Total latency: write + file fsync + directory fsync
        # WeaselDB needs directory fsync for batch file durability guarantees
        return write_latency + transfer_latency + file_fsync_latency + dir_fsync_latency

    def _handle_custom_event(self, data):
        """Handle batch completion events"""
        if data["type"] == "batch_complete":
            batch_id = data["batch_id"]
            if batch_id in self.in_flight_batches:
                batch = self.in_flight_batches.pop(batch_id)

                # Mark transactions complete
                for txn in batch["transactions"]:
                    latency_ms = (self.current_time - txn.arrival_time) * 1000
                    self.completed_transactions.append(
                        {
                            "txn_id": txn.txn_id,
                            "arrival_time": txn.arrival_time,
                            "completion_time": self.current_time,
                            "latency_ms": latency_ms,
                            "size_bytes": txn.size_bytes,
                        }
                    )

    def _calculate_storage_efficiency(self) -> float:
        return 1.2  # Some overhead for batching metadata

    def _get_durability_guarantee(self) -> str:
        return "ACID durable after EBS replication"

    def _get_consistency_model(self) -> str:
        return "Strict serializable with optimistic concurrency"

    def _get_recovery_time(self) -> float:
        return 10.0  # EBS volume attachment + recovery

    def _get_operational_complexity(self) -> int:
        return 4  # Moderate - batching logic + disk management

    def _get_infrastructure_cost(self) -> float:
        return 1.4  # Higher than S3 due to EBS provisioned storage + replication


class DatabaseComparisonFramework:
    """Framework for comparing different database persistence patterns"""

    def __init__(self, simulation_duration: float = 30.0):
        self.simulation_duration = simulation_duration

    def run_comparison(
        self, arrival_rates: List[float] = [100, 500, 1000, 2000]
    ) -> Dict[str, List[PersistenceMetrics]]:
        """Run comparison across multiple arrival rates"""

        results = defaultdict(list)

        for rate in arrival_rates:
            print(f"\nTesting at {rate} TPS...")

            # WeaselDB S3 (optimized config)
            weasel_s3 = WeaselDBSimulator(
                batch_timeout_ms=1.0,
                batch_size_threshold=800000,
                max_in_flight=50,
                simulation_duration=self.simulation_duration,
                arrival_rate_per_sec=rate,
            )
            weasel_s3_metrics = weasel_s3.run_simulation()
            results["WeaselDB S3"].append(weasel_s3_metrics)
            print(f"  WeaselDB S3 P95: {weasel_s3_metrics.p95_latency:.1f}ms")

            # WeaselDB EBS (same batching, EBS storage)
            weasel_ebs = WeaselDBDiskSimulator(
                batch_timeout_ms=1.0,
                batch_size_threshold=800000,
                max_in_flight=50,
                simulation_duration=self.simulation_duration,
                arrival_rate_per_sec=rate,
            )
            weasel_ebs_metrics = weasel_ebs.run_simulation()
            results["WeaselDB EBS"].append(weasel_ebs_metrics)
            print(f"  WeaselDB EBS P95: {weasel_ebs_metrics.p95_latency:.1f}ms")

            # Traditional WAL
            wal = TraditionalWALSimulator(
                wal_sync_interval_ms=10.0,
                simulation_duration=self.simulation_duration,
                arrival_rate_per_sec=rate,
            )
            wal_metrics = wal.run_simulation()
            results["Traditional WAL"].append(wal_metrics)
            print(f"  WAL P95: {wal_metrics.p95_latency:.1f}ms")

            # Synchronous
            sync = SynchronousSimulator(
                simulation_duration=self.simulation_duration, arrival_rate_per_sec=rate
            )
            sync_metrics = sync.run_simulation()
            results["Synchronous"].append(sync_metrics)
            print(f"  Synchronous P95: {sync_metrics.p95_latency:.1f}ms")

        return dict(results)

    def print_comparison_report(self, results: Dict[str, List[PersistenceMetrics]]):
        """Print comprehensive comparison report"""
        print("\n" + "=" * 80)
        print("DATABASE PERSISTENCE PATTERN COMPARISON")
        print("=" * 80)

        # Get arrival rates for headers
        arrival_rates = [100, 500, 1000, 2000]

        # Latency comparison
        print(f"\nP95 LATENCY COMPARISON (ms)")
        print(f"{'Pattern':<20}", end="")
        for rate in arrival_rates:
            print(f"{rate:>8} TPS", end="")
        print()
        print("-" * 60)

        for pattern_name, metrics_list in results.items():
            print(f"{pattern_name:<20}", end="")
            for metrics in metrics_list:
                print(f"{metrics.p95_latency:>8.1f}", end="")
            print()

        # Throughput comparison
        print(f"\nTHROUGHPUT ACHIEVED (TPS)")
        print(f"{'Pattern':<20}", end="")
        for rate in arrival_rates:
            print(f"{rate:>8} TPS", end="")
        print()
        print("-" * 60)

        for pattern_name, metrics_list in results.items():
            print(f"{pattern_name:<20}", end="")
            for metrics in metrics_list:
                print(f"{metrics.avg_throughput_tps:>8.1f}", end="")
            print()

        # Characteristics comparison
        print(f"\nSYSTEM CHARACTERISTICS")
        print(
            f"{'Pattern':<20} {'Durability':<25} {'Consistency':<20} {'OpComplx':<8} {'Cost':<6}"
        )
        print("-" * 85)

        for pattern_name, metrics_list in results.items():
            metrics = metrics_list[0]  # Use first metrics for characteristics
            print(
                f"{pattern_name:<20} {metrics.durability_guarantee:<25} "
                f"{metrics.consistency_model:<20} {metrics.operational_complexity:<8} "
                f"{metrics.infrastructure_cost:<6.1f}"
            )

        # Performance sweet spots
        print(f"\nPERFORMANCE SWEET SPOTS")
        print("-" * 40)

        for rate in arrival_rates:
            print(f"\nAt {rate} TPS:")
            rate_results = [
                (name, metrics_list[arrival_rates.index(rate)])
                for name, metrics_list in results.items()
            ]

            # Sort by P95 latency
            rate_results.sort(key=lambda x: x[1].p95_latency)

            for i, (name, metrics) in enumerate(rate_results):
                rank_symbol = (
                    "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else "  "
                )
                print(
                    f"  {rank_symbol} {name}: {metrics.p95_latency:.1f}ms P95, "
                    f"{metrics.avg_throughput_tps:.0f} TPS achieved"
                )

    def plot_comparison_results(
        self,
        results: Dict[str, List[PersistenceMetrics]],
        save_path: Optional[str] = None,
    ):
        """Plot comparison results"""
        try:
            arrival_rates = [100, 500, 1000, 2000]

            fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 12))
            fig.suptitle("Database Persistence Pattern Comparison", fontsize=16)

            # Plot 1: P95 Latency vs Load
            for pattern_name, metrics_list in results.items():
                p95_latencies = [m.p95_latency for m in metrics_list]
                ax1.plot(
                    arrival_rates,
                    p95_latencies,
                    marker="o",
                    linewidth=2,
                    label=pattern_name,
                )

            ax1.set_xlabel("Arrival Rate (TPS)")
            ax1.set_ylabel("P95 Latency (ms)")
            ax1.set_title("P95 Latency vs Load")
            ax1.legend()
            ax1.grid(True, alpha=0.3)
            ax1.set_yscale("log")

            # Plot 2: Throughput Achieved
            for pattern_name, metrics_list in results.items():
                throughputs = [m.avg_throughput_tps for m in metrics_list]
                ax2.plot(
                    arrival_rates,
                    throughputs,
                    marker="s",
                    linewidth=2,
                    label=pattern_name,
                )

            # Perfect throughput line
            ax2.plot(
                arrival_rates,
                arrival_rates,
                "k--",
                alpha=0.5,
                label="Perfect (no loss)",
            )

            ax2.set_xlabel("Target Rate (TPS)")
            ax2.set_ylabel("Achieved Throughput (TPS)")
            ax2.set_title("Throughput: Target vs Achieved")
            ax2.legend()
            ax2.grid(True, alpha=0.3)

            # Plot 3: Latency Distribution at 1000 TPS
            rate_idx = 2  # 1000 TPS
            for pattern_name, metrics_list in results.items():
                metrics = metrics_list[rate_idx]
                # Plot latency percentiles
                percentiles = [50, 95, 99]
                values = [
                    metrics.median_latency,
                    metrics.p95_latency,
                    metrics.p99_latency,
                ]
                ax3.bar(
                    [f"{pattern_name}\nP{p}" for p in percentiles],
                    values,
                    alpha=0.7,
                    label=pattern_name,
                )

            ax3.set_ylabel("Latency (ms)")
            ax3.set_title("Latency Percentiles at 1000 TPS")
            ax3.set_yscale("log")
            ax3.grid(True, alpha=0.3)

            # Plot 4: Cost vs Performance
            for pattern_name, metrics_list in results.items():
                costs = [m.infrastructure_cost for m in metrics_list]
                p95s = [m.p95_latency for m in metrics_list]

                # Use different markers for different patterns
                markers = {"WeaselDB": "o", "Traditional WAL": "s", "Synchronous": "^"}
                marker = markers.get(pattern_name, "o")

                ax4.scatter(
                    costs, p95s, s=100, marker=marker, alpha=0.7, label=pattern_name
                )

            ax4.set_xlabel("Infrastructure Cost (relative)")
            ax4.set_ylabel("P95 Latency (ms)")
            ax4.set_title("Cost vs Performance Trade-off")
            ax4.legend()
            ax4.grid(True, alpha=0.3)
            ax4.set_yscale("log")

            plt.tight_layout()

            if save_path:
                plt.savefig(save_path, dpi=300, bbox_inches="tight")
                print(f"Comparison plots saved to {save_path}")
            else:
                plt.show()

        except Exception as e:
            print(f"Could not generate plots: {e}")


def main():
    """Run database persistence pattern comparison"""
    print("Database Persistence Pattern Comparison")
    print("Comparing WeaselDB vs Traditional Database Approaches")
    print()

    comparison = DatabaseComparisonFramework(simulation_duration=20.0)
    results = comparison.run_comparison()

    comparison.print_comparison_report(results)

    try:
        comparison.plot_comparison_results(results, "database_comparison.png")
    except Exception as e:
        print(f"Could not generate plots: {e}")


if __name__ == "__main__":
    main()
