"""
增量监听模块
实时监听 user_preference_log_test 表的新增记录，更新用户偏好向量
"""
import logging
import math
import pymysql
import numpy as np
from typing import List, Dict, Set, Tuple, Optional
from datetime import datetime
from collections import defaultdict

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.schedulers.blocking import BlockingScheduler

from app.service.recommendation_system.config import (
    MYSQL_CONFIG, TABLE_USER_PREFERENCE_LOG, TABLE_SYS_FILE,
    RECOMMENDATION_CONFIG, REDIS_KEY_USER_PREF_PREFIX
)
from app.service.recommendation_system.vector_utils import extract_feature_vector, compute_weighted_average, normalize_vector
from app.service.recommendation_system.milvus_client import query_vectors_by_paths, insert_vectors
from app.service.utils.redis_utils import Redis
import json

logger = logging.getLogger(__name__)


class IncrementalListener:
    """增量监听器"""
    
    def __init__(self):
        self.last_process_time = None
        self.processed_combinations: Set[Tuple[int, str]] = set()  # 已处理的 (account_id, category) 组合
        self.listen_interval = RECOMMENDATION_CONFIG["listen_interval_sec"]
    
    def get_new_like_records(self) -> List[Tuple]:
        """
        获取新增点赞记录
        
        Returns:
            记录列表，每个元素为 (id, account_id, path, category, style, data_time, is_system_sketch, sys_file_id)
        """
        conn = None
        try:
            conn = pymysql.connect(**MYSQL_CONFIG)
            cursor = conn.cursor()
            
            if self.last_process_time is None:
                # 第一次运行，查询最近30分钟的数据
                cursor.execute(f"""
                    SELECT id, account_id, path, category, style, data_time, is_system_sketch, sys_file_id
                    FROM {TABLE_USER_PREFERENCE_LOG}
                    WHERE data_time > DATE_SUB(NOW(), INTERVAL 30 MINUTE)
                    ORDER BY data_time
                """)
            else:
                # 基于上次处理时间查询
                cursor.execute(f"""
                    SELECT id, account_id, path, category, style, data_time, is_system_sketch, sys_file_id
                    FROM {TABLE_USER_PREFERENCE_LOG}
                    WHERE data_time > %s
                    ORDER BY data_time
                """, (self.last_process_time,))
            
            records = cursor.fetchall()
            return records
            
        except Exception as e:
            logger.error(f"获取新增点赞记录失败: {e}", exc_info=True)
            return []
        finally:
            if conn:
                conn.close()
    
    def process_new_records(self, records: List[Tuple]):
        """
        处理新增记录
        
        Args:
            records: 记录列表
        """
        if not records:
            return
        
        # 按用户+类别分组
        user_category_records = defaultdict(list)
        for record in records:
            account_id = record[1]
            category = record[3]
            if category:  # 只处理有类别的记录
                user_category_records[(account_id, category)].append(record)
        
        # 去重：只处理一次每个 (account_id, category) 组合
        to_process = []
        for (account_id, category), recs in user_category_records.items():
            if (account_id, category) not in self.processed_combinations:
                to_process.append((account_id, category, recs))
                self.processed_combinations.add((account_id, category))
        
        logger.info(f"需要处理 {len(to_process)} 个用户-类别组合")
        
        # 处理每个组合
        for account_id, category, recs in to_process:
            try:
                self.update_user_preference_vector(account_id, category)
            except Exception as e:
                logger.error(f"更新用户偏好向量失败 [user={account_id}, category={category}]: {e}", exc_info=True)
        
        # 更新最后处理时间
        if records:
            self.last_process_time = records[-1][5]  # data_time
        # 重置去重集合，确保下次周期不会跳过同一用户-类别
        self.processed_combinations.clear()
    
    def update_user_preference_vector(self, account_id: int, category: str):
        """
        更新用户偏好向量
        
        Args:
            account_id: 用户ID
            category: 类别
        """
        conn = None
        try:
            conn = pymysql.connect(**MYSQL_CONFIG)
            cursor = conn.cursor()
            
            # 1. 获取该用户该类别的所有点赞记录
            cursor.execute(f"""
                SELECT path, data_time 
                FROM {TABLE_USER_PREFERENCE_LOG} 
                WHERE account_id = %s AND category = %s 
                ORDER BY data_time DESC
            """, (account_id, category))
            
            like_records = cursor.fetchall()
            
            if not like_records:
                return
            
            # 2. 批量查询点赞次数
            paths = [r[0] for r in like_records]
            placeholders = ','.join(['%s'] * len(paths))
            cursor.execute(f"""
                SELECT path, COUNT(*) as like_count 
                FROM {TABLE_USER_PREFERENCE_LOG} 
                WHERE account_id = %s AND category = %s AND path IN ({placeholders})
                GROUP BY path
            """, (account_id, category) + tuple(paths))
            
            like_counts = {row[0]: row[1] for row in cursor.fetchall()}
            
            # 3. 批量获取向量
            vectors_dict = query_vectors_by_paths(paths)
            
            # 处理查询不到的 path（新用户图或异常情况）
            missing_paths = [p for p in paths if p not in vectors_dict]
            if missing_paths:
                logger.info(f"用户 {account_id} 类别 {category} 有 {len(missing_paths)} 个 path 需要实时计算向量")
                self._compute_and_insert_missing_vectors(missing_paths, conn)
                # 重新查询
                vectors_dict = query_vectors_by_paths(paths)
            
            # 4. 计算权重并加权平均
            vectors = []
            weights = []
            K_half = RECOMMENDATION_CONFIG["K_half"]
            
            for k, (path, data_time) in enumerate(like_records, 1):
                if path not in vectors_dict:
                    continue
                
                vector_data = vectors_dict[path]
                feature_vector = np.array(vector_data["feature_vector"])
                
                # 时间衰减权重
                d_k = 0.5 ** (k / K_half)
                
                # 点赞次数权重
                like_count = like_counts.get(path, 1)
                p_i = 1 + math.log(1 + like_count)
                
                # 综合权重
                w_i = d_k * p_i
                
                vectors.append(feature_vector)
                weights.append(w_i)
            
            if not vectors:
                logger.warning(f"用户 {account_id} 类别 {category} 没有有效向量")
                return
            
            # 5. 计算加权平均并做 L2 归一化，IP≈cosine
            preference_vector = compute_weighted_average(vectors, weights)
            preference_vector = normalize_vector(preference_vector)
            
            # 6. 写入 Redis
            key = f"{REDIS_KEY_USER_PREF_PREFIX}:{account_id}:{category}"
            vector_json = json.dumps(preference_vector.tolist())
            Redis.write(
                key=key,
                value=vector_json,
                expire=RECOMMENDATION_CONFIG["redis_expire_seconds"]
            )
            
            logger.debug(f"用户偏好向量更新成功 [user={account_id}, category={category}]")
            
        except Exception as e:
            logger.error(f"更新用户偏好向量失败 [user={account_id}, category={category}]: {e}", exc_info=True)
            raise
        finally:
            if conn:
                conn.close()
    
    def _compute_and_insert_missing_vectors(self, paths: List[str], conn: pymysql.connections.Connection):
        """
        计算并插入缺失的向量
        
        Args:
            paths: 缺失的 path 列表
            conn: 数据库连接
        """
        cursor = conn.cursor()
        data_to_insert = []
        
        for path in paths:
            try:
                # 判断数据来源（查询 t_sys_file 表）
                cursor.execute(f"""
                    SELECT id, url, style, level3_type, level2_type, deprecated 
                    FROM {TABLE_SYS_FILE} 
                    WHERE url = %s 
                    LIMIT 1
                """, (path,))
                
                sys_file = cursor.fetchone()
                
                # 提取特征向量
                feature_vector = extract_feature_vector(path)
                
                if np.all(feature_vector == 0):
                    logger.warning(f"向量提取失败，跳过: {path}")
                    continue
                
                if sys_file:
                    # 系统图
                    sys_file_id, url, style, level3_type, level2_type, deprecated = sys_file
                    category = f"{level3_type.lower()}_{level2_type.lower()}"
                    
                    data_item = {
                        "path": path,
                        "sys_file_id": sys_file_id,
                        "style": style,
                        "category": category,
                        "is_system_sketch": 1,
                        "deprecated": deprecated if deprecated else 0,
                        "feature_vector": feature_vector.tolist()
                    }
                else:
                    # 用户图
                    # 从 user_preference_log_test 获取 category（如果有）
                    cursor.execute(f"""
                        SELECT category 
                        FROM {TABLE_USER_PREFERENCE_LOG} 
                        WHERE path = %s AND category IS NOT NULL
                        LIMIT 1
                    """, (path,))
                    
                    category_result = cursor.fetchone()
                    category = category_result[0] if category_result else None
                    
                    data_item = {
                        "path": path,
                        "sys_file_id": None,
                        "style": None,
                        "category": category,
                        "is_system_sketch": 0,
                        "deprecated": 0,
                        "feature_vector": feature_vector.tolist()
                    }
                
                data_to_insert.append(data_item)
                
            except Exception as e:
                logger.error(f"处理缺失向量失败 [{path}]: {e}")
        
        # 批量插入
        if data_to_insert:
            try:
                insert_vectors(data_to_insert)
                logger.info(f"成功插入 {len(data_to_insert)} 个缺失向量")
            except Exception as e:
                logger.error(f"插入缺失向量失败: {e}")
    
    def process_once(self):
        """单次轮询任务，供调度器调用"""
        try:
            records = self.get_new_like_records()

            if records:
                logger.info(f"发现 {len(records)} 条新增记录")
                self.process_new_records(records)
            else:
                logger.debug("没有新增记录")
        except Exception as e:
            logger.error(f"监听轮询异常: {e}", exc_info=True)


def start_background_listener(scheduler: BackgroundScheduler):
    """将增量监听任务注册到后台调度器"""
    listener = IncrementalListener()
    scheduler.add_job(
        listener.process_once,
        "interval",
        seconds=listener.listen_interval,
        max_instances=1,
        coalesce=True,
        id="recommendation_incremental_listener",
        replace_existing=True,
    )
    logger.info("增量监听任务已注册到调度器")


def start_blocking_listener():
    """以阻塞方式启动调度器（用于独立脚本运行）"""
    listener = IncrementalListener()
    scheduler = BlockingScheduler()
    scheduler.add_job(
        listener.process_once,
        "interval",
        seconds=listener.listen_interval,
        max_instances=1,
        coalesce=True,
        id="recommendation_incremental_listener",
        replace_existing=True,
    )
    logger.info("增量监听调度器已启动（BlockingScheduler）")
    scheduler.start()


if __name__ == "__main__":
    start_blocking_listener()

