"""Operation logger for tracking filesystem operations.

Provides audit trail, undo capability, and debugging support.
"""

import json
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import desc, func, select
from sqlalchemy.orm import Session

from nexus.storage.models import OperationLogModel


class OperationLogger:
    """Logs filesystem operations for audit, undo, and debugging."""

    def __init__(self, session: Session):
        """Initialize operation logger.

        Args:
            session: SQLAlchemy session for database operations.
        """
        self.session = session

    @staticmethod
    def _apply_filters(
        stmt: Any,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        operation_type: str | None = None,
        path: str | None = None,
        status: str | None = None,
        since: datetime | None = None,
        until: datetime | None = None,
        path_pattern: str | None = None,
    ) -> Any:
        """Apply shared WHERE clauses to a query.

        Args:
            stmt: Base SELECT statement.
            zone_id: Filter by zone ID.
            agent_id: Filter by agent ID.
            operation_type: Filter by operation type.
            path: Filter by path (exact match).
            status: Filter by status (success/failure).
            since: Filter created_at >= since.
            until: Filter created_at <= until.
            path_pattern: Wildcard path filter (* → SQL LIKE %).

        Returns:
            Statement with filters applied.
        """
        if zone_id is not None:
            stmt = stmt.where(OperationLogModel.zone_id == zone_id)
        if agent_id is not None:
            stmt = stmt.where(OperationLogModel.agent_id == agent_id)
        if operation_type is not None:
            stmt = stmt.where(OperationLogModel.operation_type == operation_type)
        if path is not None:
            stmt = stmt.where(OperationLogModel.path == path)
        if status is not None:
            stmt = stmt.where(OperationLogModel.status == status)
        if since is not None:
            stmt = stmt.where(OperationLogModel.created_at >= since)
        if until is not None:
            stmt = stmt.where(OperationLogModel.created_at <= until)
        if path_pattern is not None:
            # Escape SQL LIKE special chars in user input, then convert * → %
            escaped = path_pattern.replace("%", r"\%").replace("_", r"\_")
            like_pattern = escaped.replace("*", "%")
            stmt = stmt.where(OperationLogModel.path.like(like_pattern, escape="\\"))
        return stmt

    def log_operation(
        self,
        operation_type: str,
        path: str,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        new_path: str | None = None,
        snapshot_hash: str | None = None,
        metadata_snapshot: dict[str, Any] | None = None,
        status: str = "success",
        error_message: str | None = None,
        flush: bool = True,
        entity_urn: str | None = None,
        aspect_name: str | None = None,
        change_type: str | None = None,
    ) -> str:
        """Log a filesystem operation.

        Args:
            operation_type: Type of operation (write, delete, rename, etc.)
            path: Path affected by operation
            zone_id: Zone ID for multi-tenancy
            agent_id: Agent ID performing operation
            new_path: New path for rename operations
            snapshot_hash: CAS hash of previous content
            metadata_snapshot: Previous file metadata (owner, group, mode, etc.)
            status: Operation status (success/failure)
            error_message: Error message if operation failed
            flush: Whether to flush the session immediately (default True).
                Set to False in batch operations to defer the flush until commit.
            entity_urn: MCL entity URN (Issue #2929 Step 4).
            aspect_name: MCL aspect name (Issue #2929 Step 4).
            change_type: MCL change type — upsert/delete (Issue #2929 Step 4).

        Returns:
            operation_id: UUID of logged operation
        """
        # Auto-assign sequence_number with retry on unique constraint violation.
        # Uses MAX+1 within the current session. The UNIQUE constraint on
        # sequence_number prevents duplicates under concurrent writers;
        # on collision, retry with the new MAX+1 (up to 3 attempts).
        #
        # IMPORTANT: Uses a SAVEPOINT (nested transaction) so that on collision
        # only the failed INSERT is rolled back, not the caller's pending work
        # (e.g. AspectService entity_aspects rows).
        from sqlalchemy.exc import IntegrityError

        max_retries = 3
        for attempt in range(max_retries):
            next_seq = self.session.execute(
                select(func.coalesce(func.max(OperationLogModel.sequence_number), 0) + 1)
            ).scalar()

            operation = OperationLogModel(
                operation_type=operation_type,
                path=path,
                zone_id=zone_id,
                agent_id=agent_id,
                new_path=new_path,
                snapshot_hash=snapshot_hash,
                metadata_snapshot=json.dumps(metadata_snapshot) if metadata_snapshot else None,
                status=status,
                error_message=error_message,
                created_at=datetime.now(UTC),
                sequence_number=next_seq,
                entity_urn=entity_urn,
                aspect_name=aspect_name,
                change_type=change_type,
            )

            operation.validate()

            nested = self.session.begin_nested()
            self.session.add(operation)
            try:
                nested.commit()
                if flush:
                    self.session.flush()
                return operation.operation_id
            except IntegrityError:
                nested.rollback()
                if attempt < max_retries - 1:
                    continue
                raise

        return operation.operation_id  # pragma: no cover

    def replay_changes(
        self,
        *,
        from_sequence: int = 0,
        zone_id: str | None = None,
        batch_size: int = 500,
    ) -> Any:
        """Iterate operation log records that carry MCL semantics (Issue #2929).

        Yields OperationLogModel rows where entity_urn IS NOT NULL,
        ordered by sequence_number ascending. This is the operation_log-native
        equivalent of MCLRecorder.replay_changes() per Key Decision #2:
        "MCL in existing operation log, not a third event system."

        Args:
            from_sequence: Start from this sequence number (inclusive).
            zone_id: Filter by zone ID.
            batch_size: Number of records per batch (cursor-based).

        Yields:
            OperationLogModel instances with MCL columns populated.
        """
        current_seq = from_sequence
        while True:
            batch_stmt = (
                select(OperationLogModel)
                .where(
                    OperationLogModel.entity_urn.isnot(None),
                    OperationLogModel.sequence_number >= current_seq,
                )
                .order_by(OperationLogModel.sequence_number)
                .limit(batch_size)
            )
            if zone_id is not None:
                batch_stmt = batch_stmt.where(OperationLogModel.zone_id == zone_id)
            batch = list(self.session.execute(batch_stmt).scalars())
            if not batch:
                break
            yield from batch
            last_seq = batch[-1].sequence_number
            current_seq = (last_seq + 1) if last_seq is not None else current_seq + batch_size

    def get_operation(self, operation_id: str) -> OperationLogModel | None:
        """Get operation by ID.

        Args:
            operation_id: Operation UUID

        Returns:
            Operation log entry or None if not found
        """
        stmt = select(OperationLogModel).where(OperationLogModel.operation_id == operation_id)
        return self.session.execute(stmt).scalar_one_or_none()

    def list_operations(
        self,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        operation_type: str | None = None,
        path: str | None = None,
        status: str | None = None,
        since: datetime | None = None,
        until: datetime | None = None,
        path_pattern: str | None = None,
        limit: int = 100,
        offset: int = 0,
    ) -> list[OperationLogModel]:
        """List operations with optional filters.

        Args:
            zone_id: Filter by zone ID
            agent_id: Filter by agent ID
            operation_type: Filter by operation type
            path: Filter by path (exact match)
            status: Filter by status (success/failure)
            since: Filter created_at >= since
            until: Filter created_at <= until
            path_pattern: Wildcard path filter (* → SQL LIKE %)
            limit: Maximum number of results
            offset: Offset for pagination

        Returns:
            List of operation log entries, most recent first
        """
        stmt = select(OperationLogModel).order_by(desc(OperationLogModel.created_at))
        stmt = self._apply_filters(
            stmt,
            zone_id=zone_id,
            agent_id=agent_id,
            operation_type=operation_type,
            path=path,
            status=status,
            since=since,
            until=until,
            path_pattern=path_pattern,
        )
        stmt = stmt.limit(limit).offset(offset)

        return list(self.session.execute(stmt).scalars())

    def list_operations_cursor(
        self,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        operation_type: str | None = None,
        path: str | None = None,
        status: str | None = None,
        since: datetime | None = None,
        until: datetime | None = None,
        path_pattern: str | None = None,
        limit: int = 100,
        cursor: str | None = None,
    ) -> tuple[list[OperationLogModel], str | None]:
        """List operations using cursor-based pagination (Postgres Best Practice).

        Cursor-based pagination provides O(1) performance regardless of page depth,
        unlike OFFSET which gets slower on deeper pages.
        Reference: https://supabase.com/docs/guides/database/pagination

        Args:
            zone_id: Filter by zone ID
            agent_id: Filter by agent ID
            operation_type: Filter by operation type
            path: Filter by path (exact match)
            status: Filter by status (success/failure)
            since: Filter created_at >= since
            until: Filter created_at <= until
            path_pattern: Wildcard path filter (* → SQL LIKE %)
            limit: Maximum number of results
            cursor: Cursor from previous response (operation_id of last item)

        Returns:
            Tuple of (operations list, next_cursor or None if no more results)
        """
        stmt = select(OperationLogModel).order_by(
            desc(OperationLogModel.created_at),
            desc(OperationLogModel.operation_id),  # Tie-breaker for same timestamp
        )

        # Apply cursor (fetch items after this operation_id)
        if cursor:
            # Get the cursor operation to find its created_at
            cursor_op = self.session.execute(
                select(OperationLogModel).where(OperationLogModel.operation_id == cursor)
            ).scalar_one_or_none()
            if cursor_op:
                # Use composite cursor: (created_at, operation_id)
                stmt = stmt.where(
                    (OperationLogModel.created_at < cursor_op.created_at)
                    | (
                        (OperationLogModel.created_at == cursor_op.created_at)
                        & (OperationLogModel.operation_id < cursor)
                    )
                )

        stmt = self._apply_filters(
            stmt,
            zone_id=zone_id,
            agent_id=agent_id,
            operation_type=operation_type,
            path=path,
            status=status,
            since=since,
            until=until,
            path_pattern=path_pattern,
        )

        # Fetch one extra to detect if there are more results
        stmt = stmt.limit(limit + 1)

        results = list(self.session.execute(stmt).scalars())

        # Determine next cursor
        if len(results) > limit:
            # More results exist
            results = results[:limit]
            next_cursor = results[-1].operation_id
        else:
            next_cursor = None

        return results, next_cursor

    def count_operations(
        self,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        operation_type: str | None = None,
        path: str | None = None,
        status: str | None = None,
        since: datetime | None = None,
        until: datetime | None = None,
        path_pattern: str | None = None,
    ) -> int:
        """Count operations matching filters.

        Args:
            zone_id: Filter by zone ID
            agent_id: Filter by agent ID
            operation_type: Filter by operation type
            path: Filter by path (exact match)
            status: Filter by status (success/failure)
            since: Filter created_at >= since
            until: Filter created_at <= until
            path_pattern: Wildcard path filter (* → SQL LIKE %)

        Returns:
            Total count of matching operations.
        """
        stmt = select(func.count()).select_from(OperationLogModel)
        stmt = self._apply_filters(
            stmt,
            zone_id=zone_id,
            agent_id=agent_id,
            operation_type=operation_type,
            path=path,
            status=status,
            since=since,
            until=until,
            path_pattern=path_pattern,
        )
        result: int = self.session.execute(stmt).scalar_one()
        return result

    def get_last_operation(
        self,
        *,
        zone_id: str | None = None,
        agent_id: str | None = None,
        operation_type: str | None = None,
        status: str = "success",
    ) -> OperationLogModel | None:
        """Get the last successful operation.

        Args:
            zone_id: Filter by zone ID
            agent_id: Filter by agent ID
            operation_type: Filter by operation type
            status: Filter by status (default: success)

        Returns:
            Most recent operation matching filters or None
        """
        operations = self.list_operations(
            zone_id=zone_id,
            agent_id=agent_id,
            operation_type=operation_type,
            status=status,
            limit=1,
        )
        return operations[0] if operations else None

    def get_path_history(
        self,
        path: str,
        *,
        zone_id: str | None = None,
        limit: int = 50,
    ) -> list[OperationLogModel]:
        """Get operation history for a specific path.

        Args:
            path: Virtual path
            zone_id: Filter by zone ID
            limit: Maximum number of results

        Returns:
            List of operations affecting this path, most recent first
        """
        return self.list_operations(
            path=path,
            zone_id=zone_id,
            limit=limit,
        )

    def agent_activity_summary(
        self,
        *,
        agent_id: str,
        zone_id: str,
        since: datetime | None = None,
        recent_paths_limit: int = 10,
    ) -> dict[str, Any]:
        """Get aggregated activity summary for an agent.

        Executes two queries:
        1. GROUP BY operation_type → operations_by_type, total, first_seen, last_active
        2. GROUP BY path → recent_paths ordered by most recent touch

        Args:
            agent_id: Agent ID to summarize.
            zone_id: Zone ID for multi-tenancy scoping.
            since: Only include operations created_at >= since.
            recent_paths_limit: Max number of recent paths to return.

        Returns:
            Dict with keys: agent_id, total_operations, operations_by_type,
            recent_paths, first_seen, last_active.
        """
        # Query 1: Aggregate counts and timestamps by operation type
        type_stmt = select(
            OperationLogModel.operation_type,
            func.count().label("cnt"),
            func.min(OperationLogModel.created_at).label("min_ts"),
            func.max(OperationLogModel.created_at).label("max_ts"),
        ).group_by(OperationLogModel.operation_type)
        type_stmt = self._apply_filters(
            type_stmt,
            zone_id=zone_id,
            agent_id=agent_id,
            since=since,
        )
        type_rows = self.session.execute(type_stmt).all()

        operations_by_type: dict[str, int] = {}
        total_operations = 0
        first_seen: datetime | None = None
        last_active: datetime | None = None

        for row in type_rows:
            op_type, cnt, min_ts, max_ts = row
            operations_by_type[op_type] = cnt
            total_operations += cnt
            if first_seen is None or (min_ts is not None and min_ts < first_seen):
                first_seen = min_ts
            if last_active is None or (max_ts is not None and max_ts > last_active):
                last_active = max_ts

        # Query 2: Recent paths (deduplicated, ordered by most recent touch)
        paths_stmt = (
            select(
                OperationLogModel.path,
                func.max(OperationLogModel.created_at).label("last_touched"),
            )
            .group_by(OperationLogModel.path)
            .order_by(desc("last_touched"))
            .limit(recent_paths_limit)
        )
        paths_stmt = self._apply_filters(
            paths_stmt,
            zone_id=zone_id,
            agent_id=agent_id,
            since=since,
        )
        paths_rows = self.session.execute(paths_stmt).all()
        recent_paths = [row[0] for row in paths_rows]

        return {
            "agent_id": agent_id,
            "total_operations": total_operations,
            "operations_by_type": operations_by_type,
            "recent_paths": recent_paths,
            "first_seen": first_seen.isoformat() if first_seen else None,
            "last_active": last_active.isoformat() if last_active else None,
        }

    def get_metadata_snapshot(self, operation: OperationLogModel) -> dict[str, Any] | None:
        """Get metadata snapshot from operation.

        Args:
            operation: Operation log entry

        Returns:
            Metadata snapshot as dict or None
        """
        if operation.metadata_snapshot:
            parsed: dict[str, Any] = json.loads(operation.metadata_snapshot)
            return parsed
        return None
