"""
Decision Logger for Learning Pipeline
=====================================
Logs all configuration decisions and feedback for analysis and learning.
"""

import json
import time
import logging
import os
from typing import Dict, Any, Optional
import duckdb
import redis.asyncio as async_redis
from pathlib import Path

log = logging.getLogger(__name__)

# Determine safe, non-synced location for learning DB
DEFAULT_DB = None
if os.name == "nt":
    # Windows: %LOCALAPPDATA% is not synced and is user-writable
    base = Path(os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local")))
else:
    # Unix-like: XDG cache directory
    base = Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache")))

DEFAULT_DB = base / "FoundryMatch" / "learning.duckdb"

# Allow override via environment variable
DB_PATH = Path(os.environ.get("FM_LEARNING_DB_PATH", str(DEFAULT_DB)))
DB_PATH.parent.mkdir(parents=True, exist_ok=True)


class DecisionLogger:
    """
    Logs configuration decisions and match feedback for learning.
    Uses DuckDB for efficient analytics and Redis for real-time access.
    """

    def __init__(
        self,
        duckdb_path: Optional[str] = None,
        redis_conn: Optional[async_redis.Redis] = None,
    ):
        # Use safe default path if not specified
        self.duckdb_path = Path(duckdb_path) if duckdb_path else DB_PATH
        self.duckdb_path.parent.mkdir(parents=True, exist_ok=True)
        self.redis_conn = redis_conn
        self.disabled = False
        self._warned = False

        # Check if disabled via environment
        if os.getenv("FM_DISABLE_DECISION_LOGGER", "").lower() in ("1", "true", "yes"):
            self.disabled = True
            log.info("DecisionLogger disabled via environment variable")
            return

        # Try to initialize with retry and graceful degradation
        try:
            self._init_tables_with_retry()
        except Exception as e:
            # Graceful fallback: disable file logging but keep endpoint alive
            self.disabled = True
            self._warn_once(f"DecisionLogger disabled due to initialization error: {e}")

    def _init_tables_with_retry(self, attempts: int = 5, backoff_ms: int = 200):
        """Initialize DuckDB tables with retry logic for file lock issues"""
        last_error = None
        for i in range(attempts):
            try:
                with duckdb.connect(str(self.duckdb_path)) as conn:
                    self._create_tables(conn)
                log.info(f"Initialized learning tables at {self.duckdb_path}")
                return
            except duckdb.IOException as e:
                last_error = e
                if "File is already open" in str(e) and i < attempts - 1:
                    time.sleep(backoff_ms / 1000.0)
                    backoff_ms = min(backoff_ms * 2, 2000)  # Exponential backoff
                    continue
                raise
            except Exception as e:
                last_error = e
                if i < attempts - 1:
                    time.sleep(backoff_ms / 1000.0)
                    continue
                raise

        if last_error:
            raise last_error

    def _create_tables(self, conn):
        """Create the actual tables"""
        # Decision log table
        conn.execute("""
                CREATE TABLE IF NOT EXISTS decision_log (
                    ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    tenant_id TEXT,
                    job_id TEXT PRIMARY KEY,
                    segment TEXT,
                    mode TEXT,
                    blocking_choice TEXT,
                    threshold_policy TEXT,
                    config_hash TEXT,
                    sample_size INTEGER,
                    runtime_ms INTEGER,
                    auto_links INTEGER,
                    reviews INTEGER,
                    precision REAL,
                    recall REAL,
                    f_beta REAL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)

        # Feedback events table
        conn.execute("""
                CREATE TABLE IF NOT EXISTS feedback_events (
                    ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    job_id TEXT,
                    match_id TEXT,
                    outcome TEXT,  -- 'accepted', 'reverted', 'corrected'
                    corrected_target TEXT,
                    user_id TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    UNIQUE(job_id, match_id)
                )
            """)

        # AI suggestion log table
        conn.execute("""
                CREATE TABLE IF NOT EXISTS ai_suggestions (
                    ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    job_id TEXT,
                    tenant_id TEXT,
                    applied BOOLEAN,
                    edited BOOLEAN,
                    payload TEXT
                )
            """)

        # Bandit stats snapshot table (for analysis)
        conn.execute("""
                CREATE TABLE IF NOT EXISTS bandit_snapshots (
                    ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    segment TEXT,
                    arm_type TEXT,  -- 'threshold' or 'blocking'
                    arm_id TEXT,
                    trials INTEGER,
                    successes REAL,
                    mean_reward REAL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)

    def _warn_once(self, msg):
        """Warn only once to avoid log spam"""
        if not self._warned:
            log.warning(msg)
            self._warned = True

    async def log_decision(
        self,
        job_id: str,
        tenant_id: str,
        segment: str,
        mode: str,
        blocking_choice: Optional[str],
        threshold_policy: str,
        config_hash: str,
        metrics: Optional[Dict[str, Any]] = None,
        ai_context: Optional[Dict[str, Any]] = None,
    ):
        """
        Log a configuration decision.

        Args:
            job_id: Job identifier
            tenant_id: Tenant/account ID
            segment: Data segment (e.g., "saas_b2b_high_quality")
            mode: "match" or "dedupe"
            blocking_choice: Blocking strategy used (e.g., "domain_3")
            threshold_policy: Threshold policy used (e.g., "balanced")
            config_hash: Hash of full configuration
            metrics: Optional performance metrics
            ai_context: Optional AI suggestion payload metadata
        """
        if self.disabled:
            return  # No-op if disabled

        try:
            with duckdb.connect(str(self.duckdb_path)) as conn:
                conn.execute(
                    """
                    INSERT OR REPLACE INTO decision_log
                    (job_id, tenant_id, segment, mode, blocking_choice,
                     threshold_policy, config_hash, sample_size, runtime_ms,
                     auto_links, reviews, precision, recall, f_beta)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                    [
                        job_id,
                        tenant_id,
                        segment,
                        mode,
                        blocking_choice,
                        threshold_policy,
                        config_hash,
                        metrics.get("sample_size") if metrics else None,
                        metrics.get("runtime_ms") if metrics else None,
                        metrics.get("auto_links") if metrics else None,
                        metrics.get("reviews") if metrics else None,
                        metrics.get("precision") if metrics else None,
                        metrics.get("recall") if metrics else None,
                        metrics.get("f_beta") if metrics else None,
                    ],
                )

                if ai_context:
                    conn.execute(
                        """
                        INSERT INTO ai_suggestions (job_id, tenant_id, applied, edited, payload)
                        VALUES (?, ?, ?, ?, ?)
                    """,
                        [
                            job_id,
                            tenant_id,
                            ai_context.get("applied"),
                            ai_context.get("edited"),
                            json.dumps(ai_context),
                        ],
                    )

            # Also store in Redis for quick access
            if self.redis_conn:
                decision_data = {
                    "job_id": job_id,
                    "tenant_id": tenant_id,
                    "segment": segment,
                    "blocking": blocking_choice,
                    "threshold": threshold_policy,
                    "timestamp": time.time(),
                }
                await self.redis_conn.set(
                    f"decision:{job_id}",
                    json.dumps(decision_data),
                    ex=86400 * 7,  # 7 day TTL
                )

            log.info(
                f"Logged decision for job {job_id}: "
                f"segment={segment}, blocking={blocking_choice}, "
                f"threshold={threshold_policy}"
            )

        except Exception as e:
            log.error(f"Failed to log decision: {e}")

    async def log_feedback(
        self,
        job_id: str,
        match_id: str,
        outcome: str,
        user_id: str,
        corrected_target: Optional[str] = None,
    ):
        """
        Log user feedback on a match.

        Args:
            job_id: Job that produced the match
            match_id: Specific match ID
            outcome: 'accepted', 'reverted', or 'corrected'
            user_id: User who provided feedback
            corrected_target: If corrected, the right target ID
        """
        if self.disabled:
            return  # No-op if disabled

        try:
            with duckdb.connect(str(self.duckdb_path)) as conn:
                conn.execute(
                    """
                    INSERT OR IGNORE INTO feedback_events
                    (job_id, match_id, outcome, corrected_target, user_id)
                    VALUES (?, ?, ?, ?, ?)
                """,
                    [job_id, match_id, outcome, corrected_target, user_id],
                )

            log.info(f"Logged feedback for match {match_id}: {outcome}")

            # Calculate reward and trigger bandit update
            await self._process_feedback_for_learning(job_id, outcome)

        except Exception as e:
            log.error(f"Failed to log feedback: {e}")

    async def _process_feedback_for_learning(self, job_id: str, outcome: str):
        """Process feedback to update bandits"""
        # Get the decision that was made
        if self.redis_conn:
            decision_data = await self.redis_conn.get(f"decision:{job_id}")
            if decision_data:
                decision = json.loads(decision_data)

                # Calculate reward using helper
                reward = self.compute_reward(outcome=outcome)

                # Import bandits (avoid circular import)
                from .bandits import ThresholdBandit, BlockingBandit

                # Update threshold bandit
                if decision.get("threshold"):
                    threshold_bandit = ThresholdBandit(self.redis_conn)
                    await threshold_bandit.update(
                        decision["segment"], decision["threshold"], reward
                    )

                # Update blocking bandit
                if decision.get("blocking"):
                    blocking_bandit = BlockingBandit(self.redis_conn)
                    await blocking_bandit.update(
                        decision["segment"], decision["blocking"], reward
                    )

    def compute_reward(
        self,
        precision: float = None,
        recall: float = None,
        fp_count: int = 0,
        latency_ms: int = None,
        review_count: int = 0,
        outcome: str = None,
    ) -> float:
        """
        Compute reward for bandit learning.
        Can be called with either:
        1. Job metrics (precision, recall, etc.)
        2. Individual feedback outcome
        """
        if outcome:
            # Simple feedback-based reward
            if outcome == "accepted":
                return 1.0
            elif outcome == "reverted":
                return 0.0
            elif outcome == "corrected":
                return 0.3
            else:
                return 0.5

        # Metric-based reward
        reward = 0.0

        if precision is not None and recall is not None:
            # F0.5 score (emphasize precision)
            beta = 0.5
            f_beta = (
                (1 + beta**2) * (precision * recall) / ((beta**2 * precision) + recall)
                if (precision + recall) > 0
                else 0
            )
            reward = f_beta

        # Penalties
        if fp_count > 0:
            reward -= min(0.3, fp_count * 0.01)  # Cap penalty at 0.3

        if latency_ms and latency_ms > 5000:
            reward -= min(0.1, (latency_ms - 5000) / 50000)  # Penalty for slow runs

        if review_count > 100:
            reward -= min(
                0.1, (review_count - 100) / 1000
            )  # Penalty for too many reviews

        return max(0.0, min(1.0, reward))  # Clamp to [0, 1]

    async def get_segment_performance(
        self, segment: str, days: int = 7
    ) -> Dict[str, Any]:
        """Get performance metrics for a segment"""
        with duckdb.connect(str(self.duckdb_path)) as conn:
            # Get recent decisions
            decisions = conn.execute(
                """
                SELECT
                    COUNT(*) as total_jobs,
                    AVG(precision) as avg_precision,
                    AVG(recall) as avg_recall,
                    AVG(f_beta) as avg_f_beta,
                    AVG(runtime_ms) as avg_runtime_ms,
                    COUNT(DISTINCT threshold_policy) as threshold_variants,
                    COUNT(DISTINCT blocking_choice) as blocking_variants
                FROM decision_log
                WHERE segment = ?
                AND ts > CURRENT_TIMESTAMP - INTERVAL ? DAY
            """,
                [segment, days],
            ).fetchone()

            # Get feedback stats
            feedback = conn.execute(
                """
                SELECT
                    outcome,
                    COUNT(*) as count
                FROM feedback_events f
                JOIN decision_log d ON f.job_id = d.job_id
                WHERE d.segment = ?
                AND f.ts > CURRENT_TIMESTAMP - INTERVAL ? DAY
                GROUP BY outcome
            """,
                [segment, days],
            ).fetchall()

            feedback_stats = {row[0]: row[1] for row in feedback}

            return {
                "segment": segment,
                "period_days": days,
                "total_jobs": decisions[0] if decisions else 0,
                "avg_precision": decisions[1] if decisions else None,
                "avg_recall": decisions[2] if decisions else None,
                "avg_f_beta": decisions[3] if decisions else None,
                "avg_runtime_ms": decisions[4] if decisions else None,
                "config_diversity": {
                    "threshold_variants": decisions[5] if decisions else 0,
                    "blocking_variants": decisions[6] if decisions else 0,
                },
                "feedback": feedback_stats,
            }

    async def export_learning_data(self, output_path: str):
        """Export learning data for offline analysis"""
        with duckdb.connect(str(self.duckdb_path)) as conn:
            # Export to parquet for efficient analysis
            conn.execute(f"""
                COPY (
                    SELECT d.*,
                           COUNT(f.match_id) as feedback_count,
                           SUM(CASE WHEN f.outcome = 'accepted' THEN 1 ELSE 0 END) as accepted,
                           SUM(CASE WHEN f.outcome = 'reverted' THEN 1 ELSE 0 END) as reverted
                    FROM decision_log d
                    LEFT JOIN feedback_events f ON d.job_id = f.job_id
                    GROUP BY d.job_id
                ) TO '{output_path}' (FORMAT PARQUET)
            """)

            log.info(f"Exported learning data to {output_path}")
