"""Persistent log storage with rotation.

This module provides the LogStore class for storing logs to disk
in JSONL format with automatic rotation and retention policies.
"""

from __future__ import annotations

import gzip
import logging
import os
import shutil
from datetime import datetime, timedelta
from pathlib import Path
from typing import Iterator

from gaol.config import get_config_dir
from gaol.logs.models import (
    LogConfig,
    LogEntry,
    LogQuery,
    LogStats,
    RetentionPolicy,
)

logger = logging.getLogger(__name__)


def get_log_store_dir() -> Path:
    """Get the default log store directory."""
    return get_config_dir() / "logs"


class LogStore:
    """Persistent storage for container logs.

    Stores logs in JSONL (JSON Lines) format, with one file per container.
    Supports automatic rotation, compression, and retention policies.

    Example:
        >>> from gaol.logs import LogStore, LogEntry
        >>> store = LogStore()
        >>> store.append("my-container", entry)
        >>> entries = store.query("my-container", LogQuery.last_n_hours(1))
    """

    def __init__(
        self,
        store_dir: Path | None = None,
        config: LogConfig | None = None,
    ) -> None:
        """Initialize the log store.

        Args:
            store_dir: Directory for log storage.
            config: Log configuration with retention policy.
        """
        self._store_dir = store_dir or get_log_store_dir()
        self._store_dir.mkdir(parents=True, exist_ok=True)
        self.config = config or LogConfig()
        self._buffers: dict[str, list[LogEntry]] = {}

    @property
    def store_dir(self) -> Path:
        """Get the log store directory."""
        return self._store_dir

    def _get_log_file(self, container_name: str) -> Path:
        """Get the log file path for a container."""
        return self._store_dir / f"{container_name}.jsonl"

    def _get_archive_dir(self) -> Path:
        """Get the archive directory for compressed logs."""
        archive = self._store_dir / "archive"
        archive.mkdir(exist_ok=True)
        return archive

    def append(self, container_name: str, entry: LogEntry) -> None:
        """Append a log entry to storage.

        Entries are buffered and flushed periodically or when
        the buffer reaches the configured size.

        Args:
            container_name: Name of the container.
            entry: Log entry to store.
        """
        if container_name not in self._buffers:
            self._buffers[container_name] = []

        self._buffers[container_name].append(entry)

        # Flush if buffer is full
        if len(self._buffers[container_name]) >= self.config.buffer_size:
            self.flush(container_name)

    def append_many(self, container_name: str, entries: list[LogEntry]) -> None:
        """Append multiple log entries.

        Args:
            container_name: Name of the container.
            entries: List of log entries to store.
        """
        for entry in entries:
            self.append(container_name, entry)

    def flush(self, container_name: str | None = None) -> None:
        """Flush buffered entries to disk.

        Args:
            container_name: Specific container to flush (all if None).
        """
        containers = [container_name] if container_name else list(self._buffers.keys())

        for name in containers:
            if name not in self._buffers or not self._buffers[name]:
                continue

            log_file = self._get_log_file(name)
            try:
                with open(log_file, "a", encoding="utf-8") as f:
                    for entry in self._buffers[name]:
                        f.write(entry.to_jsonl() + "\n")
                self._buffers[name].clear()
                logger.debug(f"Flushed logs for {name}")
            except Exception as e:
                logger.error(f"Failed to flush logs for {name}: {e}")

    def flush_all(self) -> None:
        """Flush all buffered entries."""
        self.flush()

    def query(
        self,
        container_name: str,
        query: LogQuery | None = None,
    ) -> list[LogEntry]:
        """Query logs from storage.

        Args:
            container_name: Name of the container.
            query: Query parameters (all logs if None).

        Returns:
            List of matching log entries.
        """
        entries = []
        query = query or LogQuery()

        # Include buffered entries
        if container_name in self._buffers:
            for entry in self._buffers[container_name]:
                if entry.matches_query(query):
                    entries.append(entry)

        # Read from file
        log_file = self._get_log_file(container_name)
        if log_file.exists():
            for entry in self._read_log_file(log_file):
                if entry.matches_query(query):
                    entries.append(entry)

        # Also check archived files if query spans older dates
        if query.since and query.since < datetime.now() - timedelta(days=1):
            for entry in self._read_archived_logs(container_name, query):
                if entry.matches_query(query):
                    entries.append(entry)

        # Sort by timestamp
        entries.sort(key=lambda e: e.timestamp)

        # Apply offset and limit
        if query.offset:
            entries = entries[query.offset:]
        if query.limit:
            entries = entries[:query.limit]

        return entries

    def query_all(self, query: LogQuery | None = None) -> list[LogEntry]:
        """Query logs from all containers.

        Args:
            query: Query parameters.

        Returns:
            List of matching log entries from all containers.
        """
        all_entries: list[LogEntry] = []

        for container_name in self.list_containers():
            entries = self.query(container_name, query)
            all_entries.extend(entries)

        # Sort by timestamp
        all_entries.sort(key=lambda e: e.timestamp)

        # Apply offset and limit from query
        if query:
            if query.offset:
                all_entries = all_entries[query.offset:]
            if query.limit:
                all_entries = all_entries[:query.limit]

        return all_entries

    def _read_log_file(self, log_file: Path) -> Iterator[LogEntry]:
        """Read entries from a log file.

        Args:
            log_file: Path to the log file.

        Yields:
            LogEntry objects from the file.
        """
        try:
            with open(log_file, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if line:
                        try:
                            yield LogEntry.from_jsonl(line)
                        except Exception as e:
                            logger.warning(f"Failed to parse log line: {e}")
        except Exception as e:
            logger.error(f"Failed to read log file {log_file}: {e}")

    def _read_archived_logs(
        self,
        container_name: str,
        query: LogQuery,
    ) -> Iterator[LogEntry]:
        """Read entries from archived (compressed) log files.

        Args:
            container_name: Name of the container.
            query: Query for date filtering.

        Yields:
            LogEntry objects from archived files.
        """
        archive_dir = self._get_archive_dir()
        pattern = f"{container_name}_*.jsonl.gz"

        for archive_file in archive_dir.glob(pattern):
            # Extract date from filename
            try:
                date_str = archive_file.stem.split("_")[-1].replace(".jsonl", "")
                file_date = datetime.strptime(date_str, "%Y%m%d")

                # Skip if outside query range
                if query.since and file_date.date() < query.since.date() - timedelta(days=1):
                    continue
                if query.until and file_date.date() > query.until.date():
                    continue

            except ValueError:
                continue

            # Read compressed file
            try:
                with gzip.open(archive_file, "rt", encoding="utf-8") as f:
                    for line in f:
                        line = line.strip()
                        if line:
                            try:
                                yield LogEntry.from_jsonl(line)
                            except Exception:
                                pass
            except Exception as e:
                logger.warning(f"Failed to read archive {archive_file}: {e}")

    def list_containers(self) -> list[str]:
        """List containers with stored logs.

        Returns:
            List of container names.
        """
        containers = set()

        # From active log files
        for log_file in self._store_dir.glob("*.jsonl"):
            if log_file.name != "archive":
                containers.add(log_file.stem)

        # From buffers
        containers.update(self._buffers.keys())

        return sorted(containers)

    def get_stats(self, container_name: str) -> LogStats:
        """Get statistics for a container's logs.

        Args:
            container_name: Name of the container.

        Returns:
            LogStats with counts and sizes.
        """
        log_file = self._get_log_file(container_name)

        total_entries = 0
        total_size = 0
        oldest: datetime | None = None
        newest: datetime | None = None
        by_level: dict[str, int] = {}
        by_source: dict[str, int] = {}

        # Count buffered entries
        if container_name in self._buffers:
            for entry in self._buffers[container_name]:
                total_entries += 1
                by_level[entry.level.value] = by_level.get(entry.level.value, 0) + 1
                by_source[entry.source.value] = by_source.get(entry.source.value, 0) + 1

                if oldest is None or entry.timestamp < oldest:
                    oldest = entry.timestamp
                if newest is None or entry.timestamp > newest:
                    newest = entry.timestamp

        # Count from file
        if log_file.exists():
            total_size = log_file.stat().st_size
            for entry in self._read_log_file(log_file):
                total_entries += 1
                by_level[entry.level.value] = by_level.get(entry.level.value, 0) + 1
                by_source[entry.source.value] = by_source.get(entry.source.value, 0) + 1

                if oldest is None or entry.timestamp < oldest:
                    oldest = entry.timestamp
                if newest is None or entry.timestamp > newest:
                    newest = entry.timestamp

        # Include archived files
        archive_dir = self._get_archive_dir()
        for archive_file in archive_dir.glob(f"{container_name}_*.jsonl.gz"):
            total_size += archive_file.stat().st_size

        return LogStats(
            container_name=container_name,
            total_entries=total_entries,
            total_size_bytes=total_size,
            oldest_entry=oldest,
            newest_entry=newest,
            entries_by_level=by_level,
            entries_by_source=by_source,
        )

    def rotate(self, container_name: str | None = None) -> None:
        """Rotate log files based on retention policy.

        Compresses old logs and deletes logs exceeding retention limits.

        Args:
            container_name: Specific container to rotate (all if None).
        """
        containers = [container_name] if container_name else self.list_containers()

        for name in containers:
            self._rotate_container(name)

    def _rotate_container(self, container_name: str) -> None:
        """Rotate logs for a single container.

        Args:
            container_name: Name of the container.
        """
        # Flush any buffered entries first
        self.flush(container_name)

        log_file = self._get_log_file(container_name)
        if not log_file.exists():
            return

        policy = self.config.retention
        archive_dir = self._get_archive_dir()
        now = datetime.now()

        # Read current entries and separate by age
        current_entries: list[LogEntry] = []
        archive_entries: list[LogEntry] = []
        cutoff = now - timedelta(days=policy.compress_after_days)
        max_age_cutoff = now - timedelta(days=policy.max_age_days)

        for entry in self._read_log_file(log_file):
            if entry.timestamp < max_age_cutoff:
                # Too old, skip
                continue
            elif entry.timestamp < cutoff:
                archive_entries.append(entry)
            else:
                current_entries.append(entry)

        # Write current entries back
        with open(log_file, "w", encoding="utf-8") as f:
            for entry in current_entries:
                f.write(entry.to_jsonl() + "\n")

        # Archive old entries by date
        entries_by_date: dict[str, list[LogEntry]] = {}
        for entry in archive_entries:
            date_str = entry.timestamp.strftime("%Y%m%d")
            if date_str not in entries_by_date:
                entries_by_date[date_str] = []
            entries_by_date[date_str].append(entry)

        for date_str, entries in entries_by_date.items():
            archive_file = archive_dir / f"{container_name}_{date_str}.jsonl.gz"
            with gzip.open(archive_file, "at", encoding="utf-8") as f:
                for entry in entries:
                    f.write(entry.to_jsonl() + "\n")

        # Delete old archives
        for archive_file in archive_dir.glob(f"{container_name}_*.jsonl.gz"):
            try:
                date_str = archive_file.stem.split("_")[-1].replace(".jsonl", "")
                file_date = datetime.strptime(date_str, "%Y%m%d")
                if file_date < max_age_cutoff:
                    archive_file.unlink()
                    logger.info(f"Deleted old archive: {archive_file}")
            except Exception as e:
                logger.warning(f"Failed to process archive {archive_file}: {e}")

        # Enforce size limit
        self._enforce_size_limit(container_name, policy)

        logger.debug(f"Rotated logs for {container_name}")

    def _enforce_size_limit(
        self,
        container_name: str,
        policy: RetentionPolicy,
    ) -> None:
        """Enforce maximum size limit for a container's logs.

        Args:
            container_name: Name of the container.
            policy: Retention policy with size limits.
        """
        max_bytes = policy.max_size_mb * 1024 * 1024
        log_file = self._get_log_file(container_name)
        archive_dir = self._get_archive_dir()

        # Calculate total size
        total_size = 0
        if log_file.exists():
            total_size += log_file.stat().st_size

        archive_files = sorted(
            archive_dir.glob(f"{container_name}_*.jsonl.gz"),
            key=lambda f: f.stat().st_mtime,
        )
        for af in archive_files:
            total_size += af.stat().st_size

        # Delete oldest archives until under limit
        while total_size > max_bytes and archive_files:
            oldest = archive_files.pop(0)
            size = oldest.stat().st_size
            oldest.unlink()
            total_size -= size
            logger.info(f"Deleted archive to enforce size limit: {oldest}")

    def delete(self, container_name: str) -> None:
        """Delete all logs for a container.

        Args:
            container_name: Name of the container.
        """
        # Clear buffer
        if container_name in self._buffers:
            del self._buffers[container_name]

        # Delete main log file
        log_file = self._get_log_file(container_name)
        if log_file.exists():
            log_file.unlink()

        # Delete archives
        archive_dir = self._get_archive_dir()
        for archive_file in archive_dir.glob(f"{container_name}_*.jsonl.gz"):
            archive_file.unlink()

        logger.info(f"Deleted all logs for {container_name}")

    def export(
        self,
        container_name: str,
        output_path: Path,
        query: LogQuery | None = None,
        format: str = "jsonl",
    ) -> None:
        """Export logs to a file.

        Args:
            container_name: Name of the container.
            output_path: Path to write exported logs.
            query: Optional query to filter logs.
            format: Output format ("jsonl" or "text").
        """
        entries = self.query(container_name, query)

        with open(output_path, "w", encoding="utf-8") as f:
            for entry in entries:
                if format == "jsonl":
                    f.write(entry.to_jsonl() + "\n")
                else:
                    # Text format
                    ts = entry.timestamp.strftime("%Y-%m-%d %H:%M:%S")
                    level = entry.level.value.upper()
                    f.write(f"[{ts}] [{level}] {entry.message}\n")

        logger.info(f"Exported {len(entries)} entries to {output_path}")


# Module-level singleton
_log_store: LogStore | None = None


def get_log_store() -> LogStore:
    """Get the global LogStore instance.

    Returns:
        Shared LogStore instance.
    """
    global _log_store
    if _log_store is None:
        _log_store = LogStore()
    return _log_store
