import json

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _  # 添加gettext_lazy导入

from ..models.common import ConfigCommonModel

User = get_user_model()


class AiConfig(ConfigCommonModel):
    STATIC_CHOICES = (
        (True, _('是')),
        (False, _('否')),
    )
    # 版本选择字段
    API_VERSION_CHOICES = (
        ('v1', _('旧版API')),
        ('v2', _('新版千帆API')),
    )
    api_version = models.CharField(verbose_name=_('API版本'),
                                   max_length=2,
                                   choices=API_VERSION_CHOICES,
                                   default='v2',
                                   help_text=_('选择使用的API版本'))

    # 新版千帆API字段
    authorization_key = models.CharField(
        verbose_name=_('密钥'),
        max_length=200,
        blank=True,
        help_text=_('新版本API密钥，系统会自动添加Bearer前缀'))

    qianfan_model = models.CharField(
        verbose_name=_('千帆模型'),
        max_length=100,
        blank=True,
        default='ernie-speed-128k',
        help_text=
        _('输入千帆模型名字，千帆模型中"ernie-speed-128k", "ernie-lite-8k", "ernie-tiny-8k"为免费的。如果值为"random"，我们将从这三个免费的模型当是随机选用模型'
          ))

    qianfan_appid = models.CharField(verbose_name=_('千帆AppID'),
                                     max_length=100,
                                     blank=True,
                                     help_text=_('千帆应用ID'))

    temperature = models.FloatField(
        verbose_name=_('Temperature'),
        default=0.95,
        blank=True,
        validators=[MinValueValidator(0.01),
                    MaxValueValidator(1.0)],
        help_text=_('输出随机性控制 (0, 1.0]，默认0.95'))

    top_p = models.FloatField(
        verbose_name=_('Top P'),
        default=0.8,
        blank=True,
        validators=[MinValueValidator(0.0),
                    MaxValueValidator(1.0)],
        help_text=_('输出多样性控制 [0, 1.0]，默认0.8'))

    penalty_score = models.FloatField(
        verbose_name=_('惩罚系数'),
        default=1.0,
        blank=True,
        validators=[MinValueValidator(1.0),
                    MaxValueValidator(2.0)],
        help_text=_('重复惩罚 [1.0, 2.0]，默认1.0'))

    # 旧版字段api_key\secret_key
    api_key = models.CharField(verbose_name=_('API_KEY'),
                               max_length=200,
                               blank=True,
                               help_text=_('百度API API_KEY'))

    secret_key = models.CharField(verbose_name=_('SECRET_KEY'),
                                  max_length=200,
                                  blank=True,
                                  help_text=_('百度API SECRET_KEY'))

    keywords_num = models.PositiveIntegerField(
        verbose_name=_('关键词数据量'),
        default=3,
        validators=[MinValueValidator(0),
                    MaxValueValidator(10)],
        help_text=
        _('用来向文章中增加关键词，对应字段：meta_keywords。当于少此值时,如果下拉词有词我们则补位,数量上限为下拉词数+1,但seo推荐不要超过3个'
          ))

    editor = models.ForeignKey(User,
                               on_delete=models.CASCADE,
                               default=1,
                               verbose_name=_("绑定内容编辑"))

    get_type = models.JSONField(
        verbose_name=_('调用模式'),
        blank=True,
        default=dict,
        help_text=mark_safe(
            _('''<div>自2026-1-21后，我们引入了AI部分的type区分，以兼容老版本，我们也尽可能的将每次升级的部分详细的写进说明中。
老版本我们命名为静态模板法，type值：static_template，prompt以一句话构建，方法比较简单。
新版本我们命名为动态构建法，type值：dynamic_builder，我们将关键词、标题、内容等所需要的字段，分别约束口令，然后拼接为完整的prompt。</div><div class="collapse"><details  style="margin-top:10px">
<summary style="cursor:pointer;" >新版本说明</summary>
<p>百度千帆大模型中免费推理服务模型：ERNIE-Speed-128K、ERNIE-Lite-8K、ERNIE-Speed-8K，我们实测这几个免费模型并不一定可以按照口令产出合适格式的文章，主要问题出现在关键词的获取，文章通常是没有问题的。测试了一些收费模型，效果不错。</p>
<pre style="white-space: pre-wrap;margin:10px 0;padding:10px;border:1px solid #eee;border-radius:4px " class="bg-body-tertiary">



{
  "type": "dynamic_builder",
  "description": "动态AI文章生成器配置-优化版",
  "channels": {
    "1": {
      "num": 10,
      "description": "栏目1的AI生成配置",
      "prompt": {
        "fields": {
          "role": {
            "prompt": [
              "以专业博主身份分析百度搜索'{keyword}'的结果，列出5个长尾词，每个用〖〗包裹，并选一个易排名的长尾词用「」包裹。",
              "针对关键词'{keyword}'，请进行搜索分析，找出5个相关长尾词（〖〗标记）并推荐一个最佳长尾词（「」标记）。"
            ],
            "extract_regex": [
              "〖(.*?)〗",
              "长尾词[:：]\\s*〖(.*?)〗"
            ]
          },
          "title": {
            "prompt": [
              "请根据长尾词为'{keyword}'写一篇文章，并生成一个用『』包裹的新标题，要求标题22-38字、以提问形式包含关键词。",
              "基于分析结果，为'{keyword}'创作内容，标题用『』标注，需自然吸引读者并包含核心词。"
            ],
            "extract_regex": [
              "『(.*?)』",
              "标题[:：]\\s*『(.*?)』"
            ]
          },
          "content": {
            "prompt": [
              "【文章开始】请围绕『{title}』详细阐述，结构清晰、案例丰富。正文结束请标注【文章结束】。",
              "【文章开始】以\"{title}\"为核心展开论述，注意逻辑层次。【文章结束】"
            ],
            "extract_regex": [
              "(?<=【文章开始】)[\\s\\S]*?(?=【文章结束】)",
              "【文章开始】([\\s\\S]*?)【文章结束】"
            ]
          },
          "keywords": {
            "type": "regex",
            "num": 3,
            "extract_regex": [
              "〖(.*?)〗",
              "关键词[:：]\\s*([^，。]+)"
            ]
          }
        }
      },
      "custom_model": "ernie-speed-128k",
      "custom_temperature": 0.8,
      "custom_top_p": 0.7,
      "custom_penalty_score": 1.2
    }
  }
}
</pre>
<table><thead><tr><th><div class="ybc-p">字段路径</div></th><th><div class="ybc-p">类型</div></th><th><div class="ybc-p">必填</div></th><th><div class="ybc-p">说明</div></th><th><div class="ybc-p">可选值/示例</div></th></tr></thead><tbody><tr><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">channels.{id}.prompt.fields.{field}.prompt</code></div></td><td><div class="ybc-p">Array</div></td><td><div class="ybc-p">是</div></td><td><div class="ybc-p">提示词数组，随机选择使用</div></td><td><div class="ybc-p">包含<code class="hyc-common-markdown__code__inline">{keyword}</code>占位符的文本数组</div></td></tr><tr><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">channels.{id}.prompt.fields.{field}.extract_regex</code></div></td><td><div class="ybc-p">Array</div></td><td><div class="ybc-p">是</div></td><td><div class="ybc-p">正则表达式数组，按顺序尝试匹配</div></td><td><div class="ybc-p">如 <code class="hyc-common-markdown__code__inline">["〖(.*?)〗", "长尾词[:：]\\s*〖(.*?)〗"]</code></div></td></tr><tr><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">channels.{id}.prompt.fields.keywords.type</code></div></td><td><div class="ybc-p">String</div></td><td><div class="ybc-p">是</div></td><td><div class="ybc-p">关键词提取方式</div></td><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">dropdown</code>（下拉词）, <code class="hyc-common-markdown__code__inline">regex</code>（正则提取）</div></td></tr><tr><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">channels.{id}.prompt.fields.keywords.num</code></div></td><td><div class="ybc-p">Integer</div></td><td><div class="ybc-p">是</div></td><td><div class="ybc-p">关键词数量上限</div></td><td><div class="ybc-p">1-10</div></td></tr><tr><td><div class="ybc-p"><code class="hyc-common-markdown__code__inline">channels.{id}.prompt.fields.keywords.extract_regex</code></div></td><td><div class="ybc-p">Array</div></td><td><div class="ybc-p">条件必填</div></td><td><div class="ybc-p">正则提取时使用的模式</div></td><td><div class="ybc-p">当type为<code class="hyc-common-markdown__code__inline">regex</code>时必填</div></td></tr></tbody></table>
</details></div>
<div class="collapse"><details  style="margin-top:10px">
<summary style="cursor:pointer;" >老版本说明</summary>
<pre style="white-space: pre-wrap;margin:10px 0;padding:10px;border:1px solid #eee;border-radius:4px " class="bg-body-tertiary">
以json的形式写出每个要求生成的调用模式。调用模式分为单轮和多轮，可以理解为单轮是以一个关键词写出来的文章。多轮是我们与AI进行对话，将整个对话的过程中产生的文字给拼接生成的文章。
<strong>例：</strong>
<span class="text-success">
{
    "1": {
        "num": 10,
        "prompt": "以一个网站编辑的文笔，以“downword”为中心思想，用场景化解决问题思维写文章，不要以回答我的问题的方式写，但不要指出场景一二三等，直接输出一个成熟的文章|最近很多网友问有关“downword”相关的问题，请以网站编辑的口吻给大家解释一下。|详细的说明一下有关“downword”的内容，必要时可以模拟一些的场景化解决问题思维来说明，但是不必明显指出场景一二三等。|以“downword”为中心写一篇文章，用场景化解决问题思维写文章，要求条理分明但不要在文章当中体现是你构造的场景|详细的以“downword”为主题写一个文章，要求条理清晰，在文首写一个文章简介，但是不需要用简介来进行提示，对文中的相关事件适当举例说明，最好是从名著中找到相当的例子，不得少于500字|写一篇介绍“downword”相关的内容的文章，要求每个问题分多个方面来说明，最后是每个方面再举个例子，在文首写一个文章简介，但是不需要用简介来进行提示，不需要在文章当中告诉我该文标题是什么，要求字数超过500字",
        "h2_type": "keyword相关之downword：|keyword详解：downword|downword：",
        "get_type": "单轮",
        "title_type": "keyword（downwords[1]）|keyword：downwords[0]|keyword VS downword|keyword——downword|keyword，downword|keyword：downword？",
        // 如果使用新版本：可选的自定义模型参数，旧版本加入了也不会实现
        "custom_model": "ernie-speed-128k",
        "custom_temperature": 0.8,
        "custom_top_p": 0.7,
        "custom_penalty_score": 1.2
    },
    "2": {
        "num": 10,
        "prompt": "以一个网站编辑的文笔，以“downword”为中心思想，用场景化解决问题思维写文章，不要以回答我的问题的方式写，但不要指出场景一二三等，直接输出一个成熟的文章|最近很多网友问有关“downword”相关的问题，请以网站编辑的口吻给大家解释一下。|详细的说明一下有关“downword”的内容，必要时可以模拟一些的场景化解决问题思维来说明，但是不必明显指出场景一二三等。|以“downword”为中心写一篇文章，用场景化解决问题思维写文章，要求条理分明但不要在文章当中体现是你构造的场景|详细的以“downword”为主题写一个文章，要求条理清晰，在文首写一个文章简介，但是不需要用简介来进行提示，对文中的相关事件适当举例说明，最好是从名著中找到相当的例子，不得少于500字|写一篇介绍“downword”相关的内容的文章，要求每个问题分多个方面来说明，最后是每个方面再举个例子，在文首写一个文章简介，但是不需要用简介来进行提示，不需要在文章当中告诉我该文标题是什么，要求字数超过500字",
        "h2_type": "keyword相关之downword：|keyword详解：downword|downword：",
        "get_type": "一词多文",
        "title_type": "keyword（downword）|keyword：downword|keyword VS downword|keyword——downword|keyword，downword|keyword：downword？",
        // 在新版本中也可只自定义部分参数
        "temperature": 0.9,
        "top_p": 0.6
    }
}
</span>
<br>

<strong>"2"</strong>==》键，我们以此来选定栏目
<strong>"get_type": </strong>"单轮"==》可以选择单轮、多轮、一词多文。
<strong>"num"</strong>==》生成数。在多轮中表示循环几轮，在单轮中，无效，在一词多文中表示生成几篇文章
<strong>"title_type"</strong>==》标题生成口令：支持keyword\\downword、downwords[0]三个变量，[]内为数字，可修改，不要超过9，单轮中可以用[]中的数字来控制用第几个词取名和建立生成口令。downwords[0]这种形式，在多轮和一词多语言中无效。支持"|"分割不同口令
<strong>"prompt"</strong>==》内容生成口令。支持keyword\\downword两个变量，支持"|"分割不同口令
<strong>"h2_type"</strong>==>多轮h2标题生成口令，支持keyword\\downword两个变量，支持"|"分割不同口令，单轮和一词多文中无效

<strong>****可选的自定义模型参数****</strong>
<strong>custom_model:</strong>千帆模型的名字，如果值为random那么将会从三个免费模型"ernie-speed-128k", "ernie-lite-8k", "ernie-tiny-8k"中随机选1个
<strong>custom_temperature: </strong>说明：（1）较高的数值会使输出更加随机，而较低的数值会使其更加集中和确定。（2）默认0.95，范围 (0, 1.0]，不能为0。（3）建议该参数和top_p只设置1个。（4）建议top_p和temperature不要同时更改。,
<strong>custom_top_p: </strong>说明：（1）影响输出文本的多样性，取值越大，生成文本的多样性越强。（2）默认0.8，取值范围 [0, 1.0]。（3）建议该参数和temperature只设置1个。（4）建议top_p和temperature不要同时更改。,
<strong>custom_penalty_score: </strong>通过对已生成的token增加惩罚，减少重复生成的现象。说明：（1）值越大表示惩罚越大。（2）默认1.0，取值范围：[1.0, 2.0]。

<strong>如果要清空这里须用{}来填入,否则还是会储存旧值</strong>
</pre>
</details></div>''')))

    replace = models.TextField(verbose_name=_('文章内替换字符'),
                               blank=True,
                               help_text=_('格式"待替换字符$字符|待替换字符$字符"'))

    end = models.TextField(verbose_name=_('结束语'),
                           blank=True,
                           help_text=_('不可以有"%"号'))

    tag_switch = models.BooleanField(
        verbose_name=_('Tag AI开关'),
        default=False,
        choices=STATIC_CHOICES,
        help_text=_('在有百度API设置的情况下选择是否打开Tag通过AI生成简介。'))

    tag_ai_type = models.IntegerField(
        verbose_name=_('生成方式'),
        choices=(
            (0, _('访问触发，多进程')),
            (1, _('访问触发，同步网页更新')),
        ),
        default=1,
        help_text=
        _('访问时：当访客访问tag内容页时，如果没有简介，则通过AI生成一个简介。该行为为多进程，访问后前端页面展示和后台在AI获取是分开进行的，所以当前访问的页面大可能是没有AI简介的。）'
          ))

    tag_prompt = models.TextField(verbose_name=_('TagAI口令'),
                                  blank=True,
                                  help_text=mark_safe(
                                      _('''AI生成口令，tag词用keyword替换。
如tag词：为"上格软件"，我们要生成"上格软件有哪些优秀的软件"，我们可以设置为"keyword有哪些优秀的软件"。
我们经常使用的是"向我简单介绍一下有关"keyword"是什么意思"''')))

    tag_end = models.TextField(
        verbose_name=_('TagAI结束语'),
        blank=True,
        help_text=_('不可以有"%"号;用来在tag得到AI后加个结束语。比如告知访客本文为AI生成.tag词用keyword替换'))

    class Meta:
        verbose_name = _('AI配置')
        verbose_name_plural = _('AI配置')

    def __str__(self):
        return str(_("AI配置"))  # 使用str()包装确保返回字符串

    def clean(self):
        """数据验证"""
        super().clean()

        if self.api_version == 'v1' and not self.api_key:
            raise ValidationError({'api_key': _('使用旧版API时必须填写API_KEY')})

        if self.api_version == 'v2':
            if not self.authorization_key:
                raise ValidationError(
                    {'authorization_key': _('使用新版千帆API时必须填写密钥')})
            if not self.qianfan_appid:
                raise ValidationError(
                    {'qianfan_appid': _('使用新版千帆API时必须填写AppID')})

            # 验证temperature和top_p不同时设置
            if self.temperature != 0.95 and self.top_p != 0.8:
                raise ValidationError(
                    _('建议temperature和top_p不要同时更改，请只设置其中一个参数'))
        if self.get_type:
            try:
                if isinstance(self.get_type, str):
                    # 如果是字符串，尝试解析
                    json.loads(self.get_type)
                elif isinstance(self.get_type, dict):
                    # 如果是字典，确保可以序列化
                    json.dumps(self.get_type)
            except (json.JSONDecodeError, TypeError) as e:
                raise ValidationError({
                    'get_type': _(f'JSON格式无效: {str(e)}')
                })

    def save(self, *args, **kwargs):
        """保存时根据版本清理不需要的字段"""
        if self.api_version == 'v1':
            # 清空新版字段（可选）
            self.authorization_key = ''
            self.qianfan_appid = ''
        else:
            # 清空旧版字段（可选）
            self.api_key = ''
            self.secret_key = ''

        super().save(*args, **kwargs)
