"""
AIVA Decision Gate - Pre-Execution Gate
=========================================

The pre-execution gate that blocks or allows actions. Every task must
pass through this gate before execution. The gate produces one of four
decisions: PROCEED, CONFIRM, BLOCK, or ESCALATE.

Features:
  - Redis-based confirmation queue with TTL for pending tasks
  - PostgreSQL audit trail for every gate decision
  - Confirm/reject API for Kinan (future: Telegram/VAPI webhook)
  - Expired confirmations auto-reject with notification
  - Integration with AutonomyEngine for assessment
  - Integration with ConfidenceScorerV2 for multi-factor scoring
  - Integration with MemoryGate for historical context

VERIFICATION_STAMP
Story: AIVA-DECIDE-003
Verified By: Claude Opus 4.6
Verified At: 2026-02-11
Component: Decision Gate (pre-execution gate)

NO SQLITE. All storage uses Elestio PostgreSQL/Qdrant/Redis.
"""

import sys
import json
import logging
import time
import uuid
from pathlib import Path
from typing import Dict, Optional, Any, List
from datetime import datetime
from dataclasses import dataclass, field

# Elestio config path
GENESIS_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(GENESIS_ROOT / "data" / "genesis-memory"))

from elestio_config import PostgresConfig, RedisConfig
import psycopg2

# Redis import with fallback
try:
    import redis
    HAS_REDIS = True
except ImportError:
    redis = None
    HAS_REDIS = False

from .autonomy_engine import (
    AutonomyEngine,
    AutonomyLevel,
    AutonomyAssessment,
    GateDecision,
    get_autonomy_engine,
)

logger = logging.getLogger("AIVA.DecisionGate")


# =============================================================================
# CONSTANTS
# =============================================================================

# Redis keys for the confirmation queue
REDIS_CONFIRM_QUEUE = "aiva:decision_gate:pending"
REDIS_CONFIRM_PREFIX = "aiva:decision_gate:task:"

# Default TTL for confirmation requests (10 minutes)
DEFAULT_CONFIRM_TTL_SECONDS = 600


# =============================================================================
# DATA CLASSES
# =============================================================================

@dataclass
class GateCheckResult:
    """Result of a gate check."""
    task_id: str
    decision: GateDecision
    assessment: AutonomyAssessment
    confirmation_required: bool = False
    confirmation_ttl: int = DEFAULT_CONFIRM_TTL_SECONDS
    reasoning: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "task_id": self.task_id,
            "decision": self.decision.value,
            "assessment": self.assessment.to_dict(),
            "confirmation_required": self.confirmation_required,
            "confirmation_ttl": self.confirmation_ttl,
            "reasoning": self.reasoning,
        }


@dataclass
class PendingConfirmation:
    """A task awaiting Kinan's confirmation."""
    task_id: str
    task_type: str
    task_description: str
    assessment_summary: Dict
    created_at: str
    expires_at: str
    ttl_seconds: int

    def to_dict(self) -> Dict[str, Any]:
        return {
            "task_id": self.task_id,
            "task_type": self.task_type,
            "task_description": self.task_description,
            "assessment": self.assessment_summary,
            "created_at": self.created_at,
            "expires_at": self.expires_at,
            "ttl_seconds": self.ttl_seconds,
        }


# =============================================================================
# DECISION GATE
# =============================================================================

class DecisionGate:
    """
    Pre-execution gate that blocks or allows AIVA actions.

    Every task passes through check() which returns:
      - PROCEED:  auto-execute (Level 0-1 + confidence > threshold)
      - CONFIRM:  needs Kinan confirmation (Level 2 or low confidence)
      - BLOCK:    cannot proceed (Level 3 or very low confidence)
      - ESCALATE: unusual situation, needs human review

    Confirmation queue is stored in Redis with TTL.
    All decisions are logged to PostgreSQL for audit trail.

    Usage:
        gate = DecisionGate()

        result = gate.check(
            task_type="read_file",
            task_description="Read system logs",
        )

        if result.decision == GateDecision.PROCEED:
            execute_task(...)
        elif result.decision == GateDecision.CONFIRM:
            # Task is in Redis queue, waiting for Kinan
            print(f"Waiting for confirmation: {result.task_id}")
        elif result.decision == GateDecision.BLOCK:
            print(f"Blocked: {result.reasoning}")
    """

    def __init__(
        self,
        engine: Optional[AutonomyEngine] = None,
        confirm_ttl: int = DEFAULT_CONFIRM_TTL_SECONDS,
    ):
        """
        Initialize the decision gate.

        Args:
            engine: Optional AutonomyEngine instance (uses singleton if not provided)
            confirm_ttl: Default TTL for confirmation requests (seconds)
        """
        self._engine = engine or get_autonomy_engine()
        self._confirm_ttl = confirm_ttl
        self._redis = None
        self._db_conn = None

        self._init_redis()
        self._ensure_tables()

        logger.info("DecisionGate initialized")

    def _init_redis(self):
        """Initialize Redis connection for confirmation queue."""
        if not HAS_REDIS:
            logger.warning("DecisionGate: redis-py not installed, confirmation queue disabled")
            return
        try:
            self._redis = redis.Redis(**RedisConfig.get_connection_params())
            self._redis.ping()
            logger.info("DecisionGate: Redis connected for confirmation queue")
        except Exception as e:
            self._redis = None
            logger.warning(f"DecisionGate: Redis unavailable - {e}")

    def _get_db_connection(self):
        """Get or create PostgreSQL connection."""
        if self._db_conn is None or self._db_conn.closed:
            self._db_conn = psycopg2.connect(**PostgresConfig.get_connection_params())
        return self._db_conn

    def _ensure_tables(self):
        """Create gate decision audit table."""
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS aiva_gate_decisions (
                    id SERIAL PRIMARY KEY,
                    task_id TEXT NOT NULL,
                    task_type TEXT NOT NULL,
                    task_description TEXT,
                    autonomy_level INT,
                    confidence_score FLOAT,
                    risk_score FLOAT,
                    gate_decision TEXT NOT NULL,
                    confirmed_by TEXT,
                    confirmed_at TIMESTAMP,
                    rejected_by TEXT,
                    rejection_reason TEXT,
                    expired BOOLEAN DEFAULT FALSE,
                    created_at TIMESTAMP DEFAULT NOW()
                )
            """)
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_gate_decisions_task_id
                ON aiva_gate_decisions(task_id)
            """)
            cursor.execute("""
                CREATE INDEX IF NOT EXISTS idx_gate_decisions_pending
                ON aiva_gate_decisions(gate_decision, confirmed_by)
                WHERE gate_decision = 'confirm' AND confirmed_by IS NULL
            """)
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.warning(f"Gate table creation skipped (non-fatal): {e}")

    # =========================================================================
    # MAIN API
    # =========================================================================

    def check(
        self,
        task_type: str,
        task_description: str = "",
        task_id: Optional[str] = None,
        memory_context: Optional[Dict] = None,
    ) -> GateCheckResult:
        """
        Check a task against the decision gate.

        This is the main entry point. Runs the task through the
        AutonomyEngine assessment and returns a gate decision.

        If the decision is CONFIRM, the task is added to the Redis
        confirmation queue with a TTL.

        Args:
            task_type: Machine-readable task type
            task_description: Human-readable description
            task_id: Optional task ID (auto-generated if not provided)
            memory_context: Decision context from MemoryGate

        Returns:
            GateCheckResult with decision and details
        """
        if task_id is None:
            task_id = f"gate_{uuid.uuid4().hex[:12]}"

        # Run through autonomy engine
        assessment = self._engine.assess_task(
            task_type=task_type,
            task_description=task_description,
            task_id=task_id,
            memory_context=memory_context,
        )

        decision = assessment.gate_decision
        confirmation_required = decision == GateDecision.CONFIRM

        result = GateCheckResult(
            task_id=task_id,
            decision=decision,
            assessment=assessment,
            confirmation_required=confirmation_required,
            confirmation_ttl=self._confirm_ttl,
            reasoning=assessment.reasoning,
        )

        # If confirmation required, add to Redis queue
        if confirmation_required:
            self._add_to_confirmation_queue(task_id, assessment)

        # Log to PostgreSQL audit trail
        self._log_gate_decision(task_id, assessment, decision)

        logger.info(
            f"Gate check: task={task_id} type={task_type} "
            f"decision={decision.value}"
        )

        return result

    def confirm(
        self,
        task_id: str,
        approved_by: str = "Kinan",
    ) -> bool:
        """
        Confirm a blocked task. Kinan approves execution.

        Args:
            task_id: Task ID to confirm
            approved_by: Who approved it

        Returns:
            True if confirmation was successful, False if task not found/expired
        """
        # Remove from Redis queue
        removed = self._remove_from_confirmation_queue(task_id)

        # Update PostgreSQL audit
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE aiva_gate_decisions
                SET confirmed_by = %s, confirmed_at = NOW()
                WHERE task_id = %s
                AND gate_decision = 'confirm'
                AND confirmed_by IS NULL
                AND rejected_by IS NULL
            """, (approved_by, task_id))
            rows_updated = cursor.rowcount
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.error(f"Confirm DB update failed: {e}")
            rows_updated = 0

        success = removed or rows_updated > 0

        if success:
            logger.info(f"Task {task_id} CONFIRMED by {approved_by}")
        else:
            logger.warning(f"Task {task_id} not found or already processed")

        return success

    def reject(
        self,
        task_id: str,
        reason: str = "Rejected by user",
        rejected_by: str = "Kinan",
    ) -> bool:
        """
        Reject a blocked task.

        Args:
            task_id: Task ID to reject
            reason: Rejection reason
            rejected_by: Who rejected it

        Returns:
            True if rejection was successful
        """
        # Remove from Redis queue
        self._remove_from_confirmation_queue(task_id)

        # Update PostgreSQL audit
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE aiva_gate_decisions
                SET rejected_by = %s, rejection_reason = %s
                WHERE task_id = %s
                AND gate_decision = 'confirm'
                AND confirmed_by IS NULL
                AND rejected_by IS NULL
            """, (rejected_by, reason, task_id))
            rows_updated = cursor.rowcount
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.error(f"Reject DB update failed: {e}")
            rows_updated = 0

        success = rows_updated > 0
        if success:
            logger.info(f"Task {task_id} REJECTED by {rejected_by}: {reason}")
        else:
            logger.warning(f"Task {task_id} not found or already processed")

        return success

    def get_pending_confirmations(self) -> List[PendingConfirmation]:
        """
        Get all tasks currently awaiting confirmation.

        Returns:
            List of PendingConfirmation objects
        """
        pending = []

        # Try Redis first (fast)
        if self._redis:
            try:
                members = self._redis.smembers(REDIS_CONFIRM_QUEUE)
                for task_id_bytes in members:
                    task_id = (
                        task_id_bytes.decode()
                        if isinstance(task_id_bytes, bytes)
                        else task_id_bytes
                    )
                    key = f"{REDIS_CONFIRM_PREFIX}{task_id}"
                    data = self._redis.get(key)
                    if data:
                        item = json.loads(data)
                        pending.append(PendingConfirmation(
                            task_id=task_id,
                            task_type=item.get("task_type", "unknown"),
                            task_description=item.get("task_description", ""),
                            assessment_summary=item.get("assessment", {}),
                            created_at=item.get("created_at", ""),
                            expires_at=item.get("expires_at", ""),
                            ttl_seconds=item.get("ttl_seconds", 0),
                        ))
                    else:
                        # Key expired but set member still exists; clean up
                        self._redis.srem(REDIS_CONFIRM_QUEUE, task_id)
            except Exception as e:
                logger.warning(f"Redis pending query failed: {e}")

        # Fallback to PostgreSQL if Redis unavailable
        if not pending and not self._redis:
            try:
                conn = self._get_db_connection()
                cursor = conn.cursor()
                cursor.execute("""
                    SELECT task_id, task_type, task_description,
                           autonomy_level, confidence_score, risk_score,
                           created_at
                    FROM aiva_gate_decisions
                    WHERE gate_decision = 'confirm'
                    AND confirmed_by IS NULL
                    AND rejected_by IS NULL
                    AND expired = FALSE
                    ORDER BY created_at ASC
                """)
                for row in cursor.fetchall():
                    pending.append(PendingConfirmation(
                        task_id=row[0],
                        task_type=row[1],
                        task_description=row[2] or "",
                        assessment_summary={
                            "autonomy_level": row[3],
                            "confidence_score": row[4],
                            "risk_score": row[5],
                        },
                        created_at=row[6].isoformat() if row[6] else "",
                        expires_at="",
                        ttl_seconds=self._confirm_ttl,
                    ))
                cursor.close()
            except Exception as e:
                logger.warning(f"PostgreSQL pending query failed: {e}")

        return pending

    def cleanup_expired(self) -> int:
        """
        Clean up expired confirmation requests.
        Marks them as expired in PostgreSQL and removes from Redis.

        Returns:
            Number of expired confirmations cleaned up
        """
        cleaned = 0

        # Redis cleanup (TTL handles expiry, just clean the set)
        if self._redis:
            try:
                members = self._redis.smembers(REDIS_CONFIRM_QUEUE)
                for task_id_bytes in members:
                    task_id = (
                        task_id_bytes.decode()
                        if isinstance(task_id_bytes, bytes)
                        else task_id_bytes
                    )
                    key = f"{REDIS_CONFIRM_PREFIX}{task_id}"
                    if not self._redis.exists(key):
                        # Key expired, clean up set
                        self._redis.srem(REDIS_CONFIRM_QUEUE, task_id)
                        cleaned += 1

                        # Mark as expired in PostgreSQL
                        self._mark_expired(task_id)
            except Exception as e:
                logger.warning(f"Redis cleanup failed: {e}")

        # PostgreSQL cleanup for old unprocessed entries
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE aiva_gate_decisions
                SET expired = TRUE
                WHERE gate_decision = 'confirm'
                AND confirmed_by IS NULL
                AND rejected_by IS NULL
                AND expired = FALSE
                AND created_at < NOW() - INTERVAL '%s seconds'
                RETURNING task_id
            """, (self._confirm_ttl,))
            expired_rows = cursor.fetchall()
            conn.commit()
            cursor.close()
            cleaned += len(expired_rows)
        except Exception as e:
            logger.warning(f"PostgreSQL cleanup failed: {e}")

        if cleaned > 0:
            logger.info(f"Cleaned up {cleaned} expired confirmations")

        return cleaned

    # =========================================================================
    # INTERNAL: REDIS CONFIRMATION QUEUE
    # =========================================================================

    def _add_to_confirmation_queue(
        self, task_id: str, assessment: AutonomyAssessment
    ) -> None:
        """Add a task to the Redis confirmation queue with TTL."""
        if not self._redis:
            logger.warning(
                f"Redis unavailable; task {task_id} pending in PostgreSQL only"
            )
            return

        try:
            now = datetime.now()
            data = {
                "task_id": task_id,
                "task_type": assessment.task_type,
                "task_description": assessment.task_description,
                "assessment": assessment.to_dict(),
                "created_at": now.isoformat(),
                "expires_at": "",
                "ttl_seconds": self._confirm_ttl,
            }

            key = f"{REDIS_CONFIRM_PREFIX}{task_id}"

            # Store task data with TTL
            self._redis.setex(key, self._confirm_ttl, json.dumps(data))

            # Add to the pending set (for listing)
            self._redis.sadd(REDIS_CONFIRM_QUEUE, task_id)

            logger.info(
                f"Task {task_id} added to confirmation queue "
                f"(TTL={self._confirm_ttl}s)"
            )
        except Exception as e:
            logger.error(f"Failed to add task to Redis queue: {e}")

    def _remove_from_confirmation_queue(self, task_id: str) -> bool:
        """Remove a task from the Redis confirmation queue."""
        if not self._redis:
            return False

        try:
            key = f"{REDIS_CONFIRM_PREFIX}{task_id}"
            removed_key = self._redis.delete(key)
            removed_set = self._redis.srem(REDIS_CONFIRM_QUEUE, task_id)
            return (removed_key + removed_set) > 0
        except Exception as e:
            logger.warning(f"Redis remove failed: {e}")
            return False

    # =========================================================================
    # INTERNAL: POSTGRESQL AUDIT
    # =========================================================================

    def _log_gate_decision(
        self,
        task_id: str,
        assessment: AutonomyAssessment,
        decision: GateDecision,
    ) -> None:
        """Log gate decision to PostgreSQL audit trail."""
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO aiva_gate_decisions (
                    task_id, task_type, task_description,
                    autonomy_level, confidence_score, risk_score,
                    gate_decision
                ) VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (
                task_id,
                assessment.task_type,
                assessment.task_description,
                assessment.autonomy_level.value,
                assessment.confidence_score,
                assessment.risk_score,
                decision.value,
            ))
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.warning(f"Gate decision audit log failed: {e}")

    def _mark_expired(self, task_id: str) -> None:
        """Mark a confirmation as expired in PostgreSQL."""
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE aiva_gate_decisions
                SET expired = TRUE
                WHERE task_id = %s
                AND gate_decision = 'confirm'
                AND confirmed_by IS NULL
                AND rejected_by IS NULL
            """, (task_id,))
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.warning(f"Mark expired failed: {e}")

    # =========================================================================
    # STATUS
    # =========================================================================

    def get_status(self) -> Dict[str, Any]:
        """Get decision gate status."""
        pending = self.get_pending_confirmations()

        # Get recent decision stats from PostgreSQL
        recent_stats = {}
        try:
            conn = self._get_db_connection()
            cursor = conn.cursor()
            cursor.execute("""
                SELECT
                    gate_decision,
                    COUNT(*) as count
                FROM aiva_gate_decisions
                WHERE created_at > NOW() - INTERVAL '24 hours'
                GROUP BY gate_decision
            """)
            for row in cursor.fetchall():
                recent_stats[row[0]] = row[1]
            cursor.close()
        except Exception as e:
            logger.debug(f"Gate stats query failed: {e}")

        return {
            "redis_connected": self._redis is not None,
            "pending_confirmations": len(pending),
            "confirm_ttl_seconds": self._confirm_ttl,
            "recent_24h": recent_stats,
            "engine_status": self._engine.get_status(),
        }

    # =========================================================================
    # CLEANUP
    # =========================================================================

    def close(self):
        """Close all connections."""
        if self._redis:
            try:
                self._redis.close()
            except Exception:
                pass
        if self._db_conn and not self._db_conn.closed:
            self._db_conn.close()
        logger.info("DecisionGate closed")


# =============================================================================
# MODULE-LEVEL SINGLETON
# =============================================================================

_gate_instance: Optional[DecisionGate] = None


def get_decision_gate() -> DecisionGate:
    """
    Get or create the singleton DecisionGate instance.

    Returns:
        DecisionGate instance
    """
    global _gate_instance
    if _gate_instance is None:
        _gate_instance = DecisionGate()
    return _gate_instance
