import json
import re
import threading
import base64
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict

from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.common import retry
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
from app.utils.url import UrlUtils

lock = threading.Lock()


class RetryException(Exception):
    pass


class WeChat:
    # 企业微信Token
    _access_token = None
    # 企业微信Token过期时间
    _expires_in: int = None
    # 企业微信Token获取时间
    _access_token_time: datetime = None
    # 企业微信CorpID
    _corpid = None
    # 企业微信AppSecret
    _appsecret = None
    # 企业微信AppID
    _appid = None
    # 代理
    _proxy = None

    # 企业微信发送消息URL
    _send_msg_url = "cgi-bin/message/send?access_token={access_token}"
    # 企业微信获取TokenURL
    _token_url = "cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}"
    # 企业微信创建菜单URL
    _create_menu_url = "cgi-bin/menu/create?access_token={access_token}&agentid={agentid}"
    # 企业微信删除菜单URL
    _delete_menu_url = "cgi-bin/menu/delete?access_token={access_token}&agentid={agentid}"
    # 企业微信下载媒体URL
    _download_media_url = "cgi-bin/media/get?access_token={access_token}&media_id={media_id}"
    # 企业微信上传临时素材URL
    _upload_media_url = "cgi-bin/media/upload?access_token={access_token}&type={media_type}"

    def __init__(self, WECHAT_CORPID: Optional[str] = None, WECHAT_APP_SECRET: Optional[str] = None,
                 WECHAT_APP_ID: Optional[str] = None, WECHAT_PROXY: Optional[str] = None, **kwargs):
        """
        初始化
        """
        if not WECHAT_CORPID or not WECHAT_APP_SECRET or not WECHAT_APP_ID:
            logger.error("企业微信配置不完整！")
            return
        self._corpid = WECHAT_CORPID
        self._appsecret = WECHAT_APP_SECRET
        self._appid = WECHAT_APP_ID
        self._proxy = WECHAT_PROXY or "https://qyapi.weixin.qq.com"

        if self._proxy:
            self._send_msg_url = UrlUtils.adapt_request_url(self._proxy, self._send_msg_url)
            self._token_url = UrlUtils.adapt_request_url(self._proxy, self._token_url)
            self._create_menu_url = UrlUtils.adapt_request_url(self._proxy, self._create_menu_url)
            self._delete_menu_url = UrlUtils.adapt_request_url(self._proxy, self._delete_menu_url)
            self._download_media_url = UrlUtils.adapt_request_url(self._proxy, self._download_media_url)
            self._upload_media_url = UrlUtils.adapt_request_url(self._proxy, self._upload_media_url)

        if self._corpid and self._appsecret and self._appid:
            self.__get_access_token()

    def get_state(self):
        """
        获取状态
        """
        return True if self.__get_access_token() else False

    @retry(RetryException, logger=logger)
    def __get_access_token(self, force=False):
        """
        获取微信Token
        :return： 微信Token
        """
        token_flag = True
        if not self._access_token:
            token_flag = False
        else:
            if (datetime.now() - self._access_token_time).seconds >= self._expires_in:
                token_flag = False

        if not token_flag or force:
            if not self._corpid or not self._appsecret:
                return None
            token_url = self._token_url.format(corpid=self._corpid, corpsecret=self._appsecret)
            res = RequestUtils().get_res(token_url)
            if res:
                ret_json = res.json()
                if ret_json.get("errcode") == 0:
                    self._access_token = ret_json.get("access_token")
                    self._expires_in = ret_json.get("expires_in")
                    self._access_token_time = datetime.now()
            elif res is not None:
                logger.error(f"获取微信access_token失败，错误码：{res.status_code}，错误原因：{res.reason}")
            else:
                logger.error(f"获取微信access_token失败，未获取到返回信息")
                raise RetryException("获取微信access_token失败，重试中...")
        return self._access_token

    @staticmethod
    def __split_content(content: str, max_bytes: int = 2048) -> List[str]:
        """
        将内容分块为不超过 max_bytes 字节的块
        :param content: 待拆分的内容
        :param max_bytes: 最大字节数
        :return: 分块后的内容列表
        """
        content_chunks = []
        current_chunk = bytearray()

        for line in content.splitlines():
            encoded_line = (line + "\n").encode("utf-8")
            line_length = len(encoded_line)

            if line_length > max_bytes:
                # 在处理长行之前，先将 current_chunk 添加到 content_chunks
                if current_chunk:
                    content_chunks.append(current_chunk.decode("utf-8", errors="replace").strip())
                    current_chunk = bytearray()

                # 处理长行，拆分为多个不超过 max_bytes 的块
                start = 0
                while start < line_length:
                    end = start + max_bytes  # 不再需要为 "..." 预留空间
                    if end >= line_length:
                        end = line_length
                    else:
                        # 调整以避免拆分多字节字符
                        while end > start and (encoded_line[end] & 0xC0) == 0x80:
                            end -= 1
                        if end == start:
                            # 单个字符超过了 max_bytes，强制包含整个字符
                            end = start + 1
                            while end < line_length and (encoded_line[end] & 0xC0) == 0x80:
                                end += 1
                    truncated_line = encoded_line[start:end].decode("utf-8", errors="replace")
                    content_chunks.append(truncated_line.strip())
                    start = end
                continue  # 继续处理下一行

            # 检查添加当前行后是否会超过 max_bytes
            if len(current_chunk) + line_length > max_bytes:
                # 将 current_chunk 添加到 content_chunks
                content_chunks.append(current_chunk.decode("utf-8", errors="replace").strip())
                current_chunk = bytearray()

            # 将当前行添加到 current_chunk
            current_chunk += encoded_line

        # 处理剩余的 current_chunk
        if current_chunk:
            content_chunks.append(current_chunk.decode("utf-8", errors="replace").strip())

        return content_chunks

    def __send_message(self, title: str, text: Optional[str] = None,
                       userid: Optional[str] = None, link: Optional[str] = None) -> bool:
        """
        发送文本消息
        :param title: 消息标题
        :param text: 消息内容
        :param userid: 消息发送对象的ID，为空则发给所有人
        :param link: 跳转链接
        :return: 发送状态，错误信息
        """
        if not title and not text:
            logger.error("消息标题和内容不能都为空")
            return False
        if text:
            formatted_text = text.replace("\n\n", "\n")
            content = f"{title}\n{formatted_text}"
        else:
            content = title
        if link:
            content = f"{content}\n点击查看：{link}"
        if not userid:
            userid = "@all"
        # 分块处理逻辑
        content_chunks = self.__split_content(content)
        # 逐块发送消息
        for chunk in content_chunks:
            req_json = {
                "touser": userid,
                "msgtype": "text",
                "agentid": self._appid,
                "text": {
                    "content": chunk
                },
                "safe": 0,
                "enable_id_trans": 0,
                "enable_duplicate_check": 0
            }
            try:
                # 如果是超长消息，有一个发送失败就全部失败
                if not self.__post_request(self._send_msg_url, req_json):
                    return False
            except Exception as e:
                logger.error(f"发送消息块失败：{e}")
                return False
        return True

    def __send_image_message(self, title: str, text: str, image_url: str,
                             userid: Optional[str] = None, link: Optional[str] = None) -> Optional[bool]:
        """
        发送图文消息
        :param title: 消息标题
        :param text: 消息内容
        :param image_url: 图片地址
        :param userid: 消息发送对象的ID，为空则发给所有人
        :param link: 跳转链接
        :return: 发送状态，错误信息
        """
        if text:
            text = text.replace("\n\n", "\n")
        if not userid:
            userid = "@all"
        req_json = {
            "touser": userid,
            "msgtype": "news",
            "agentid": self._appid,
            "news": {
                "articles": [
                    {
                        "title": title,
                        "description": text,
                        "picurl": image_url,
                        "url": link
                    }
                ]
            }
        }
        try:
            return self.__post_request(self._send_msg_url, req_json)
        except Exception as e:
            logger.error(f"发送图文消息失败：{e}")
            return False

    def send_msg(self, title: str, text: Optional[str] = None, image: Optional[str] = None,
                 userid: Optional[str] = None, link: Optional[str] = None) -> Optional[bool]:
        """
        微信消息发送入口，支持文本、图片、链接跳转、指定发送对象
        :param title: 消息标题
        :param text: 消息内容
        :param image: 图片地址
        :param userid: 消息发送对象的ID，为空则发给所有人
        :param link: 跳转链接
        :return: 发送状态，错误信息
        """
        try:
            if not self.__get_access_token():
                logger.error("获取微信access_token失败，请检查参数配置")
                return None

            if image:
                ret_code = self.__send_image_message(title=title, text=text, image_url=image, userid=userid, link=link)
            else:
                ret_code = self.__send_message(title=title, text=text, userid=userid, link=link)

            return ret_code
        except Exception as e:
            logger.error(f"发送消息失败：{e}")
            return False

    @staticmethod
    def _guess_mime_type(content: bytes, default: str = "image/jpeg") -> str:
        """
        根据文件头推断图片 MIME
        """
        if not content:
            return default
        if content.startswith(b"\x89PNG\r\n\x1a\n"):
            return "image/png"
        if content.startswith(b"\xff\xd8\xff"):
            return "image/jpeg"
        if content.startswith((b"GIF87a", b"GIF89a")):
            return "image/gif"
        if content.startswith(b"BM"):
            return "image/bmp"
        if content.startswith(b"RIFF") and b"WEBP" in content[:16]:
            return "image/webp"
        return default

    def download_media_to_data_url(self, media_id: str) -> Optional[str]:
        """
        下载企业微信媒体文件并转换为 data URL
        """
        if not media_id:
            return None
        access_token = self.__get_access_token()
        if not access_token:
            logger.error("下载企业微信媒体失败：access_token 获取失败")
            return None
        req_url = self._download_media_url.format(
            access_token=access_token,
            media_id=media_id,
        )
        try:
            res = RequestUtils(timeout=30).get_res(req_url)
        except Exception as err:
            logger.error(f"下载企业微信媒体失败：{err}")
            return None
        if not res or not res.content:
            return None

        content_type = (res.headers.get("Content-Type") or "").split(";")[0].strip()
        if content_type == "application/json":
            try:
                logger.error(f"企业微信媒体下载失败：{res.json()}")
            except Exception:
                logger.error(f"企业微信媒体下载失败：{res.text}")
            return None

        mime_type = self._guess_mime_type(res.content, content_type or "image/jpeg")
        return f"data:{mime_type};base64,{base64.b64encode(res.content).decode()}"

    def download_media_bytes(self, media_id: str) -> Optional[bytes]:
        """
        下载企业微信媒体文件并返回原始字节。
        """
        if not media_id:
            return None
        access_token = self.__get_access_token()
        if not access_token:
            logger.error("下载企业微信媒体失败：access_token 获取失败")
            return None
        req_url = self._download_media_url.format(
            access_token=access_token,
            media_id=media_id,
        )
        try:
            res = RequestUtils(timeout=30).get_res(req_url)
        except Exception as err:
            logger.error(f"下载企业微信媒体失败：{err}")
            return None
        if not res or not res.content:
            return None
        content_type = (res.headers.get("Content-Type") or "").split(";")[0].strip()
        if content_type == "application/json":
            try:
                logger.error(f"企业微信媒体下载失败：{res.json()}")
            except Exception:
                logger.error(f"企业微信媒体下载失败：{res.text}")
            return None
        return res.content

    @staticmethod
    def _convert_voice_to_amr(voice_path: str) -> Optional[Path]:
        """
        将语音文件转换为企业微信要求的 AMR 格式（<=60s）。
        """
        src_path = Path(voice_path)
        if not src_path.exists():
            logger.error(f"语音文件不存在：{src_path}")
            return None

        dst_path = src_path.with_suffix(".amr")
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            str(src_path),
            "-ar",
            "8000",
            "-ac",
            "1",
            "-t",
            "60",
            str(dst_path),
        ]
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=False,
            )
        except Exception as err:
            logger.error(f"调用 ffmpeg 转换 AMR 失败：{err}")
            return None

        if result.returncode != 0 or not dst_path.exists():
            logger.error(
                "ffmpeg 转换 AMR 失败: returncode=%s, stderr=%s",
                result.returncode,
                (result.stderr or "").strip()[:500],
            )
            return None

        if dst_path.stat().st_size > 2 * 1024 * 1024:
            logger.error("AMR 语音文件超过 2MB，无法发送到企业微信")
            dst_path.unlink(missing_ok=True)
            return None
        return dst_path

    def _upload_temp_media(self, media_path: Path, media_type: str = "voice") -> Optional[str]:
        """
        上传企业微信临时素材，返回 media_id。
        """
        access_token = self.__get_access_token()
        if not access_token:
            return None
        req_url = self._upload_media_url.format(
            access_token=access_token,
            media_type=media_type,
        )
        try:
            # Read file content into memory to avoid file handle issues
            with media_path.open("rb") as media_file:
                file_content = media_file.read()

            # Use RequestUtils with Accept header to avoid Content-Type conflicts
            response = RequestUtils(timeout=60).request(
                method="post",
                url=req_url,
                headers={"Accept": "application/json"},
                files={
                    "media": (
                        media_path.name,
                        file_content,
                        "voice/amr" if media_type == "voice" else "application/octet-stream",
                    )
                },
            )
        except Exception as err:
            logger.error(f"上传企业微信临时素材失败：{err}")
            return None

        if not response:
            return None

        try:
            ret_json = response.json()
        except Exception as err:
            logger.error(f"解析企业微信临时素材响应失败：{err}")
            return None

        if ret_json.get("errcode") != 0:
            logger.error(f"上传企业微信临时素材失败：{ret_json}")
            return None
        return ret_json.get("media_id")

    def send_voice(self, voice_path: str, userid: Optional[str] = None) -> Optional[bool]:
        """
        发送企业微信语音消息。仅自建应用模式支持。
        """
        if not voice_path:
            return False
        if not self.__get_access_token():
            logger.error("获取微信access_token失败，请检查参数配置")
            return None
        if not userid:
            userid = "@all"

        source_path = Path(voice_path)
        converted_path = self._convert_voice_to_amr(voice_path)
        if not converted_path:
            return False

        try:
            media_id = self._upload_temp_media(converted_path, media_type="voice")
            if not media_id:
                return False

            req_json = {
                "touser": userid,
                "msgtype": "voice",
                "agentid": self._appid,
                "voice": {
                    "media_id": media_id
                },
                "safe": 0,
                "enable_id_trans": 0,
                "enable_duplicate_check": 0
            }
            return self.__post_request(self._send_msg_url, req_json)
        except Exception as err:
            logger.error(f"发送企业微信语音消息失败：{err}")
            return False
        finally:
            converted_path.unlink(missing_ok=True)
            source_path.unlink(missing_ok=True)

    def send_medias_msg(self, medias: List[MediaInfo], userid: Optional[str] = None) -> Optional[bool]:
        """
        发送列表类消息
        """
        try:
            if not self.__get_access_token():
                logger.error("获取微信access_token失败，请检查参数配置")
                return None

            if not userid:
                userid = "@all"
            articles = []
            index = 1
            for media in medias:
                if media.vote_average:
                    title = f"{index}. {media.title_year}\n类型：{media.type.value}，评分：{media.vote_average}"
                else:
                    title = f"{index}. {media.title_year}\n类型：{media.type.value}"
                articles.append({
                    "title": title,
                    "description": "",
                    "picurl": media.get_message_image() if index == 1 else media.get_poster_image(),
                    "url": media.detail_link
                })
                index += 1

            req_json = {
                "touser": userid,
                "msgtype": "news",
                "agentid": self._appid,
                "news": {
                    "articles": articles
                }
            }
            return self.__post_request(self._send_msg_url, req_json)
        except Exception as e:
            logger.error(f"发送消息失败：{e}")
            return False

    def send_torrents_msg(self, torrents: List[Context],
                          userid: Optional[str] = None, title: Optional[str] = None,
                          link: Optional[str] = None) -> Optional[bool]:
        """
        发送列表消息
        """
        try:
            if not self.__get_access_token():
                logger.error("获取微信access_token失败，请检查参数配置")
                return None

            # 先发送标题
            if title:
                self.__send_message(title=title, userid=userid, link=link)

            # 发送列表
            if not userid:
                userid = "@all"
            articles = []
            index = 1
            for context in torrents:
                torrent = context.torrent_info
                meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
                mediainfo = context.media_info
                torrent_title = f"{index}.【{torrent.site_name}】" \
                                f"{meta.season_episode} " \
                                f"{meta.resource_term} " \
                                f"{meta.video_term} " \
                                f"{meta.release_group} " \
                                f"{StringUtils.str_filesize(torrent.size)} " \
                                f"{torrent.volume_factor} " \
                                f"{torrent.seeders}↑"
                torrent_title = re.sub(r"\s+", " ", torrent_title).strip()
                articles.append({
                    "title": torrent_title,
                    "description": torrent.description if index == 1 else "",
                    "picurl": mediainfo.get_message_image() if index == 1 else "",
                    "url": torrent.page_url
                })
                index += 1

            req_json = {
                "touser": userid,
                "msgtype": "news",
                "agentid": self._appid,
                "news": {
                    "articles": articles
                }
            }
            return self.__post_request(self._send_msg_url, req_json)
        except Exception as e:
            logger.error(f"发送消息失败：{e}")
            return False

    @retry(RetryException, logger=logger)
    def __post_request(self, url: str, req_json: dict) -> bool:
        """
        向微信发送请求
        """
        url = url.format(access_token=self.__get_access_token())
        res = RequestUtils(content_type="application/json").post(
            url=url,
            data=json.dumps(req_json, ensure_ascii=False).encode("utf-8")
        )
        if res is None:
            error_msg = "发送请求失败，未获取到返回信息"
            raise Exception(error_msg)
        if res.status_code != 200:
            error_msg = f"发送请求失败，错误码：{res.status_code}，错误原因：{res.reason}"
            raise Exception(error_msg)

        ret_json = res.json()
        if ret_json.get("errcode") == 0:
            return True
        else:
            if ret_json.get("errcode") == 42001:
                self.__get_access_token(force=True)
                error_msg = (f"access_token 已过期，尝试重新获取 access_token,"
                             f"errcode: {ret_json.get('errcode')}, errmsg: {ret_json.get('errmsg')}")
                raise RetryException(error_msg)
            else:
                logger.error(f"发送请求失败，错误信息：{ret_json.get('errmsg')}")
                return False

    def create_menus(self, commands: Dict[str, dict]):
        """
        自动注册微信菜单
        :param commands: 命令字典
        命令字典：
        {
            "/cookiecloud": {
                "func": CookieCloudChain(self._db).remote_sync,
                "description": "同步站点",
                "category": "站点",
                "data": {}
            }
        }
        注册报文格式，一级菜单只有最多3条，子菜单最多只有5条：
        {
           "button":[
               {
                   "type":"click",
                   "name":"今日歌曲",
                   "key":"V1001_TODAY_MUSIC"
               },
               {
                   "name":"菜单",
                   "sub_button":[
                       {
                           "type":"view",
                           "name":"搜索",
                           "url":"https://www.soso.com/"
                       },
                       {
                           "type":"click",
                           "name":"赞一下我们",
                           "key":"V1001_GOOD"
                       }
                   ]
              }
           ]
        }
        """
        try:
            # 请求URL
            req_url = self._create_menu_url.format(access_token="{access_token}", agentid=self._appid)

            # 对commands按category分组
            category_dict = {}
            for key, value in commands.items():
                category: str = value.get("category")
                if category:
                    if not category_dict.get(category):
                        category_dict[category] = {}
                    category_dict[category][key] = value

            # 一级菜单
            buttons = []
            for category, menu in category_dict.items():
                # 二级菜单
                sub_buttons = []
                for key, value in menu.items():
                    sub_buttons.append({
                        "type": "click",
                        "name": value.get("description"),
                        "key": key
                    })
                buttons.append({
                    "name": category,
                    "sub_button": sub_buttons[:5]
                })

            if buttons:
                # 发送请求
                self.__post_request(req_url, {
                    "button": buttons[:3]
                })
        except Exception as e:
            logger.error(f"创建菜单失败：{e}")

    def delete_menus(self):
        """
        删除微信菜单
        """
        try:
            # 请求URL
            req_url = self._delete_menu_url.format(access_token=self.__get_access_token(), agentid=self._appid)
            # 发送请求
            RequestUtils().get(req_url)
        except Exception as e:
            logger.error(f"删除菜单失败：{e}")
