import asyncio
from enum import Enum
from io import BytesIO
import math
from pathlib import Path
import random
import signal
import time

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont

from nonebot import logger

from konabot.common.database import DatabaseManager
from konabot.common.path import ASSETS_PATH, FONTS_PATH
from konabot.common.path import DATA_PATH
import nonebot
import json

ROOT_PATH = Path(__file__).resolve().parent

# 创建全局数据库管理器实例
db_manager = DatabaseManager()

class CrashType(Enum):
    BURNT = 0
    FROZEN = 1

driver = nonebot.get_driver()

@driver.on_startup
async def register_startup_hook():
    await ac_manager.start_auto_save()

@driver.on_shutdown
async def register_shutdown_hook():
    """注册关闭时需要执行的函数"""
    # 停止自动保存任务
    if ac_manager:
        await ac_manager.stop_auto_save()

class AirConditionerManager:
    def __init__(self, save_interval: int = 300):  # 默认5分钟保存一次
        self.save_interval = save_interval
        self._save_task = None
        self._running = False
        
    async def start_auto_save(self):
        """启动自动保存任务"""
        self._running = True
        self._save_task = asyncio.create_task(self._auto_save_loop())
            
        logger.info(f"自动保存任务已启动，间隔: {self.save_interval}秒")
    
    async def stop_auto_save(self):
        """停止自动保存任务"""
        if self._save_task:
            self._running = False
            self._save_task.cancel()
            try:
                await self._save_task
            except asyncio.CancelledError:
                pass
            logger.info("自动保存任务已停止")
        else:
            logger.warning("没有正在运行的自动保存任务")
        
    async def _auto_save_loop(self):
        """自动保存循环"""
        while self._running:
            try:
                await asyncio.sleep(self.save_interval)
                await self.save_all_instances()
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"定时保存失败: {e}")
                
    async def save_all_instances(self):
        save_time = time.time()
        to_remove = []
        """保存所有实例到数据库"""
        for ac_id, ac_instance in AirConditioner.InstancesPool.items():
            try:
                await db_manager.execute_by_sql_file(
                    ROOT_PATH / "sql" / "update_ac.sql",
                    [(ac_instance.on, ac_instance.temperature, 
                     ac_instance.burnt, ac_instance.frozen, ac_id),(ac_id,)]
                )
                if(save_time - ac_instance.instance_get_time >= 300): # 5 分钟
                    to_remove.append(ac_id)
            except Exception as e:
                logger.error(f"保存空调 {ac_id} 失败: {e}")

        logger.info(f"定时保存完成，共保存 {len(AirConditioner.InstancesPool)} 个空调实例")

        # 删除时间过长实例
        for ac_id in to_remove:
            del AirConditioner.InstancesPool[ac_id]

        logger.info(f"清理长期不活跃的空调实例完成，目前池内共有 {len(AirConditioner.InstancesPool)} 个实例")

ac_manager = AirConditionerManager(save_interval=300) # 5分钟

class AirConditioner:
    InstancesPool: dict[str, 'AirConditioner'] = {}

    @classmethod
    async def refresh_ac(cls, id: str):
        cls.InstancesPool[id].instance_get_time = time.time()

    @classmethod
    async def storage_ac(cls, id: str, ac: 'AirConditioner'):
        cls.InstancesPool[id] = ac

    @classmethod
    async def get_ac(cls, id: str) -> 'AirConditioner':
        if(id in cls.InstancesPool):
            await cls.refresh_ac(id)
            return cls.InstancesPool[id]
        # 如果没有，那么从数据库重新实例化一个 AC 出来
        result = await db_manager.query_by_sql_file(ROOT_PATH / "sql" / "query_ac.sql", (id,))
        if len(result) == 0:
            ac = await cls.create_ac(id)
            return ac
        ac_data = result[0]
        ac = AirConditioner(id)
        ac.on = bool(ac_data["on"])
        ac.temperature = float(ac_data["temperature"])
        ac.burnt = bool(ac_data["burnt"])
        ac.frozen = bool(ac_data["frozen"])
        await cls.storage_ac(id, ac)
        return ac

    @classmethod
    async def create_ac(cls, id: str) -> 'AirConditioner':
        ac = AirConditioner(id)
        await db_manager.execute_by_sql_file(
            ROOT_PATH / "sql" / "insert_ac.sql",
            [(id, ac.on, ac.temperature, ac.burnt, ac.frozen),(id,)]
        )
        await cls.storage_ac(id, ac)
        return ac
    
    async def change_ac_temp(self, temperature_delta: float) -> None:
        '''
        改变空调的温度
        :param temperature_delta: float 温度变化量
        '''
        changed_temp = self.temperature + temperature_delta
        random_poss = random.random()
        if temperature_delta < 0 and changed_temp < 0:
            # 根据温度随机出是否冻结，0度开始，呈指数增长
            possibility = -math.e ** (changed_temp / 50) + 1
            if random_poss < possibility:
                await self.broke_ac(CrashType.FROZEN)
        elif temperature_delta > 0 and changed_temp > 40:
            # 根据温度随机出是否烧坏，40度开始，呈指数增长
            possibility = -math.e ** ((40-changed_temp) / 50) + 1
            if random_poss < possibility:
                await self.broke_ac(CrashType.BURNT)
        self.temperature = changed_temp

    async def update_ac(self, state: bool = None, temperature_delta: float = None, burnt: bool = None, frozen: bool = None) -> 'AirConditioner':
        if state is not None:
            self.on = state
        if temperature_delta is not None:
            await self.change_ac_temp(temperature_delta)
        if burnt is not None:
            self.burnt = burnt
        if frozen is not None:
            self.frozen = frozen
        # await db_manager.execute_by_sql_file(
        #     ROOT_PATH / "sql" / "update_ac.sql",
        #     (self.on, self.temperature, self.burnt, self.frozen, self.id)
        # )
        return self

    async def change_ac(self) -> 'AirConditioner':
        self.on = False
        self.temperature = 24
        self.burnt = False
        self.frozen = False
        # await db_manager.execute_by_sql_file(
        #     ROOT_PATH / "sql" / "update_ac.sql",
        #     (self.on, self.temperature, self.burnt, self.frozen, self.id)
        # )
        return self

    def __init__(self, id: str) -> None:
        self.id = id
        self.on = False
        self.temperature = 24  # 默认温度
        self.burnt = False
        self.frozen = False

        self.instance_get_time = time.time()

    async def broke_ac(self, crash_type: CrashType):
        '''
        让空调坏掉
        :param crash_type: CrashType 枚举，表示空调坏掉的类型
        '''
        match crash_type:
            case CrashType.BURNT:
                await self.update_ac(burnt=True)
            case CrashType.FROZEN:
                await self.update_ac(frozen=True)
        await db_manager.execute_by_sql_file(
            ROOT_PATH / "sql" / "insert_crash.sql",
            (self.id, crash_type.value)
        )

    # def save_crash_data(self, crash_type: CrashType):
    #     '''
    #     如果空调爆炸了，就往本地的 ac_crash_data.json 里该 id 的记录加一
    #     '''
    #     data_file = DATA_PATH / "ac_crash_data.json"
    #     crash_data = {}
    #     if data_file.exists():
    #         with open(data_file, "r", encoding="utf-8") as f:
    #             crash_data = json.load(f)
    #     if self.id not in crash_data:
    #         crash_data[self.id] = {"burnt": 0, "frozen": 0}
    #     match crash_type:
    #         case CrashType.BURNT:
    #             crash_data[self.id]["burnt"] += 1
    #         case CrashType.FROZEN:
    #             crash_data[self.id]["frozen"] += 1
    #     with open(data_file, "w", encoding="utf-8") as f:
    #         json.dump(crash_data, f, ensure_ascii=False, indent=4)

    def get_crashes_and_ranking(self) -> tuple[int, int]:
        '''
        获取该群在全国空调损坏的数量与排行榜的位置
        '''
        data_file = DATA_PATH / "ac_crash_data.json"
        if not data_file.exists():
            return 0, 1
        with open(data_file, "r", encoding="utf-8") as f:
            crash_data = json.load(f)
        ranking_list = []
        for gid, record in crash_data.items():
            total = record.get("burnt", 0) + record.get("frozen", 0)
            ranking_list.append((gid, total))
        ranking_list.sort(key=lambda x: x[1], reverse=True)
        total_crashes = crash_data.get(self.id, {}).get("burnt", 0) + crash_data.get(self.id, {}).get("frozen", 0)
        rank = 1
        for gid, total in ranking_list:
            if gid == self.id:
                break
            rank += 1
        return total_crashes, rank

def text_to_transparent_image(text, font_size=40, padding=0, text_color=(0, 0, 0)):
    """
    将文本转换为带透明背景的图像，图像大小刚好包含文本
    """
    # 创建临时图像来计算文本尺寸
    temp_image = Image.new('RGB', (1, 1), (255, 255, 255))
    temp_draw = ImageDraw.Draw(temp_image)
    
    font = ImageFont.truetype(FONTS_PATH / "montserrat.otf", font_size)
    
    # 获取文本边界框
    bbox = temp_draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]
    
    # 计算图像大小（文本大小 + 内边距）
    image_width = int(text_width + 2 * padding)
    image_height = int(text_height + 2 * padding)
    
    # 创建RGBA模式的空白图像（带透明通道）
    image = Image.new('RGBA', (image_width, image_height), (0, 0, 0, 0))
    draw = ImageDraw.Draw(image)
    
    # 绘制文本（考虑内边距）
    x = padding - bbox[0]  # 调整起始位置
    y = padding - bbox[1]
    
    # 设置文本颜色（带透明度）
    if len(text_color) == 3:
        text_color = text_color + (255,)  # 添加完全不透明的alpha值
    
    draw.text((x, y), text, fill=text_color, font=font)
    
    # 转换为OpenCV格式（BGRA）
    image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA)
    return image_cv

def perspective_transform(image, target, corners):
    """
    对图像进行透视变换（保持透明通道）
    target: 画布
    corners: 四个角点的坐标，顺序为 [左上, 右上, 右下, 左下]
    """
    height, width = image.shape[:2]
    
    # 源点（原始图像的四个角）
    src_points = np.array([
        [0, 0],           # 左上
        [width-1, 0],     # 右上
        [width-1, height-1], # 右下
        [0, height-1]     # 左下
    ], dtype=np.float32)
    
    # 目标点（变换后的四个角）
    dst_points = np.array(corners, dtype=np.float32)
    
    # 计算透视变换矩阵
    matrix = cv2.getPerspectiveTransform(src_points, dst_points)
    
    # 获取画布大小
    target_height, target_width = target.shape[:2]

    # 应用透视变换（保持所有通道，包括alpha）
    transformed = cv2.warpPerspective(image, matrix, (target_width, target_height), flags=cv2.INTER_LINEAR)

    return transformed, matrix

def blend_with_transparency(background, foreground, position):
    """
    将带透明通道的前景图像合成到背景图像上
    position: 前景图像在背景图像上的位置 (x, y)
    """
    bg = background.copy()
    
    # 如果背景没有alpha通道，添加一个
    if bg.shape[2] == 3:
        bg = cv2.cvtColor(bg, cv2.COLOR_BGR2BGRA)
        bg[:, :, 3] = 255  # 完全不透明
    
    x, y = position
    fg_height, fg_width = foreground.shape[:2]
    bg_height, bg_width = bg.shape[:2]
    
    # 确保位置在图像范围内
    x = max(0, min(x, bg_width - fg_width))
    y = max(0, min(y, bg_height - fg_height))
    
    # 提取前景的alpha通道并归一化
    alpha_foreground = foreground[:, :, 3] / 255.0
    
    # 对于每个颜色通道进行合成
    for c in range(3):
        bg_region = bg[y:y+fg_height, x:x+fg_width, c]
        fg_region = foreground[:, :, c]
        
        # alpha混合公式
        bg[y:y+fg_height, x:x+fg_width, c] = (
            alpha_foreground * fg_region + 
            (1 - alpha_foreground) * bg_region
        )
    
    # 更新背景的alpha通道（如果需要）
    bg_alpha_region = bg[y:y+fg_height, x:x+fg_width, 3]
    bg[y:y+fg_height, x:x+fg_width, 3] = np.maximum(bg_alpha_region, foreground[:, :, 3])
    
    return bg

def precise_blend_with_perspective(background, foreground, corners):
    """
    精确合成：根据四个角点将前景图像透视合成到背景上
    """
    # 创建与背景相同大小的空白图像
    bg_height, bg_width = background.shape[:2]
    
    # 如果背景没有alpha通道，转换为BGRA
    if background.shape[2] == 3:
        background_bgra = cv2.cvtColor(background, cv2.COLOR_BGR2BGRA)
    else:
        background_bgra = background.copy()
    
    # 创建与背景相同大小的前景图层
    foreground_layer = np.zeros((bg_height, bg_width, 4), dtype=np.uint8)
    
    # 计算前景图像在背景中的边界框
    min_x = int(min(corners[:, 0]))
    max_x = int(max(corners[:, 0]))
    min_y = int(min(corners[:, 1]))
    max_y = int(max(corners[:, 1]))
    
    # 将变换后的前景图像放置到对应位置
    fg_height, fg_width = foreground.shape[:2]
    if min_y + fg_height <= bg_height and min_x + fg_width <= bg_width:
        foreground_layer[min_y:min_y+fg_height, min_x:min_x+fg_width] = foreground
    
    # 创建掩码（只在前景有内容的地方合成）
    mask = (foreground_layer[:, :, 3] > 0)
    
    # 合成图像
    result = background_bgra.copy()
    for c in range(3):
        result[:, :, c][mask] = foreground_layer[:, :, c][mask]
    result[:, :, 3][mask] = foreground_layer[:, :, 3][mask]
    
    return result

def wiggle_transform(image, intensity=2) -> list[np.ndarray]:
    '''
    返回一组图像振动的帧组，模拟空调运作时的抖动效果
    '''
    frames = []
    height, width = image.shape[:2]
    shifts = [(-intensity, 0), (intensity, 0), (0, -intensity), (0, intensity), (0, 0)]
    for dx, dy in shifts:
        M = np.float32([[1, 0, dx], [0, 1, dy]])
        shifted = cv2.warpAffine(image, M, (width, height))
        frames.append(shifted)
    return frames

async def generate_ac_image(ac: AirConditioner) -> BytesIO:
    # 找到空调底图
    ac_image = cv2.imread(str(ASSETS_PATH / "img" / "ac" / "ac.png"), cv2.IMREAD_UNCHANGED)

    if not ac.on:
        # 空调关闭状态，直接返回底图
        pil_final = Image.fromarray(ac_image)
        output = BytesIO()
        pil_final.save(output, format="GIF")
        return output

    # 根据生成温度文本图像
    text = f"{round(ac.temperature, 1)}°C"
    text_image = text_to_transparent_image(
        text, 
        font_size=60, 
        text_color=(0, 0, 0)  # 黑色文字
    )

    # 获取长宽比
    height, width = text_image.shape[:2]
    aspect_ratio = width / height

    # 定义3D变换的四个角点（透视效果）
    # 顺序: [左上, 右上, 右下, 左下]
    corners = np.array([
        [123, 45],    # 左上
        [284, 101],   # 右上
        [290, 140],   # 右下
        [119, 100]    # 左下
    ], dtype=np.float32)
    
    # 对文本图像进行3D变换（保持透明通道）
    transformed_text, transform_matrix = perspective_transform(text_image, ac_image, corners)

    final_image_simple = blend_with_transparency(ac_image, transformed_text, (0, 0))

    intensity = max(2, abs(int(ac.temperature) - 24) // 2)

    frames = wiggle_transform(final_image_simple, intensity=intensity)
    pil_frames = [Image.fromarray(frame) for frame in frames]
    output = BytesIO()
    pil_frames[0].save(output, format="GIF", save_all=True, append_images=pil_frames[1:], loop=0, duration=50, disposal=2)
    return output