from dataclasses import dataclass
from types import SimpleNamespace
from typing import Any

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.crypto import decrypt_secret, encrypt_secret
from app.config import settings as app_settings
from app.models.system_setting import SystemSetting

_MASK = "********"


@dataclass(frozen=True)
class SettingSpec:
    key: str
    default: Any
    label: str
    description: str
    value_type: str
    min_value: int | None
    max_value: int | None
    unit: str
    group: str


SETTING_SPECS: dict[str, SettingSpec] = {
    "refund.full_refund_days": SettingSpec(
        key="refund.full_refund_days",
        default=3,
        label="全额退订天数",
        description="订单开通后多少天内可全额退订。",
        value_type="integer",
        min_value=0,
        max_value=30,
        unit="天",
        group="refund",
    ),
    "refund.full_refund_limit": SettingSpec(
        key="refund.full_refund_limit",
        default=3,
        label="全额退订次数",
        description="每个用户最多可使用的全额退订次数。",
        value_type="integer",
        min_value=0,
        max_value=3,
        unit="次",
        group="refund",
    ),
    "lifecycle.expire_suspend_after_days": SettingSpec(
        key="lifecycle.expire_suspend_after_days",
        default=0,
        label="到期暂停天数",
        description="实例到期后多少天自动暂停，0 表示到期立即暂停。",
        value_type="integer",
        min_value=0,
        max_value=30,
        unit="天",
        group="lifecycle",
    ),
    "lifecycle.expire_delete_after_days": SettingSpec(
        key="lifecycle.expire_delete_after_days",
        default=3,
        label="到期删除天数",
        description="实例到期后多少天自动强制删除。",
        value_type="integer",
        min_value=1,
        max_value=365,
        unit="天",
        group="lifecycle",
    ),
    "upstream.task_sync_interval_seconds": SettingSpec(
        key="upstream.task_sync_interval_seconds",
        default=120,
        label="任务同步频率",
        description="上游资源操作任务状态同步间隔，单位秒。",
        value_type="integer",
        min_value=30,
        max_value=3600,
        unit="秒",
        group="upstream",
    ),
    "upstream.stock_sync_interval_minutes": SettingSpec(
        key="upstream.stock_sync_interval_minutes",
        default=10,
        label="魔方库存同步频率",
        description="已绑定魔方财务产品的上游库存同步间隔，单位分钟。",
        value_type="integer",
        min_value=5,
        max_value=1440,
        unit="分钟",
        group="upstream",
    ),
    "platform.site_name": SettingSpec(
        key="platform.site_name",
        default="清云互联",
        label="站点名称",
        description="用于后台展示、支付订单名称和部分通知文案。",
        value_type="text",
        min_value=2,
        max_value=40,
        unit="",
        group="platform",
    ),
    "platform.registration_enabled": SettingSpec(
        key="platform.registration_enabled",
        default=True,
        label="开放新用户注册",
        description="关闭后新用户无法获取注册验证码或完成注册，已有用户登录不受影响。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="platform",
    ),
    "platform.frontend_url": SettingSpec(
        key="platform.frontend_url",
        default=app_settings.FRONTEND_URL,
        label="前台访问地址",
        description="用于邮件通知、跳转链接等场景。",
        value_type="url",
        min_value=8,
        max_value=200,
        unit="",
        group="platform",
    ),
    "platform.backend_url": SettingSpec(
        key="platform.backend_url",
        default=app_settings.PUBLIC_BACKEND_URL,
        label="后端公开访问地址",
        description="用于魔方财务等上游系统回调本站后端，例如 https://api.example.com。",
        value_type="url",
        min_value=0,
        max_value=200,
        unit="",
        group="platform",
    ),
    "platform.promotion_base_url": SettingSpec(
        key="platform.promotion_base_url",
        default=app_settings.FRONTEND_URL,
        label="推广链接基础地址",
        description="推广码生成注册链接时使用的前台地址。",
        value_type="url",
        min_value=8,
        max_value=200,
        unit="",
        group="platform",
    ),
    "finance.recharge_min_cents": SettingSpec(
        key="finance.recharge_min_cents",
        default=max(1, int(app_settings.RECHARGE_MIN_YUAN * 100)),
        label="最低充值金额",
        description="用户单笔充值的最低金额。",
        value_type="integer",
        min_value=1,
        max_value=1_000_000,
        unit="分",
        group="finance",
    ),
    "finance.recharge_max_cents": SettingSpec(
        key="finance.recharge_max_cents",
        default=max(1, int(app_settings.RECHARGE_MAX_YUAN * 100)),
        label="最高充值金额",
        description="用户单笔充值的最高金额。",
        value_type="integer",
        min_value=1,
        max_value=100_000_000,
        unit="分",
        group="finance",
    ),
    "finance.pending_payment_ttl_minutes": SettingSpec(
        key="finance.pending_payment_ttl_minutes",
        default=120,
        label="待支付保留时间",
        description="充值订单创建后，前台保留待支付入口的时间。",
        value_type="integer",
        min_value=1,
        max_value=120,
        unit="分钟",
        group="finance",
    ),
    "finance.recharge_enabled": SettingSpec(
        key="finance.recharge_enabled",
        default=True,
        label="开放余额充值",
        description="关闭后用户不能创建新的充值订单，已创建订单和支付回调仍会继续处理。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="finance",
    ),
    "payment.enabled_plugins": SettingSpec(
        key="payment.enabled_plugins",
        default="",
        label="已启用支付插件",
        description="逗号分隔的支付插件 ID 列表。支付插件允许多个同时启用。",
        value_type="text",
        min_value=0,
        max_value=500,
        unit="",
        group="payment",
    ),
    "payment.epay.api_url": SettingSpec(
        key="payment.epay.api_url",
        default=app_settings.EPAY_API_URL,
        label="易支付 API 地址",
        description="易支付站点根地址，不带结尾斜杠。",
        value_type="url",
        min_value=0,
        max_value=200,
        unit="",
        group="payment",
    ),
    "payment.epay.pid": SettingSpec(
        key="payment.epay.pid",
        default=app_settings.EPAY_PID,
        label="易支付商户 PID",
        description="易支付商户 ID。",
        value_type="text",
        min_value=0,
        max_value=100,
        unit="",
        group="payment",
    ),
    "payment.epay.key": SettingSpec(
        key="payment.epay.key",
        default=app_settings.EPAY_KEY,
        label="易支付商户密钥",
        description="用于创建支付订单和验签回调。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="payment",
    ),
    "payment.epay.notify_url": SettingSpec(
        key="payment.epay.notify_url",
        default=app_settings.EPAY_NOTIFY_URL,
        label="易支付回调基础地址",
        description="不填时自动使用后端公开地址；系统会自动追加插件 ID 和订单校验 token。",
        value_type="url",
        min_value=0,
        max_value=300,
        unit="",
        group="payment",
    ),
    "payment.epay.return_url": SettingSpec(
        key="payment.epay.return_url",
        default=app_settings.EPAY_RETURN_URL,
        label="易支付同步跳转地址",
        description="支付完成后跳回前台的地址。",
        value_type="url",
        min_value=0,
        max_value=300,
        unit="",
        group="payment",
    ),
    "payment.epay.mapi_path": SettingSpec(
        key="payment.epay.mapi_path",
        default="/mapi.php",
        label="易支付创建订单路径",
        description="不同易支付系统可能是 /mapi.php 或 /xpay/epay/mapi.php。",
        value_type="text",
        min_value=1,
        max_value=160,
        unit="",
        group="payment",
    ),
    "payment.epay.api_path": SettingSpec(
        key="payment.epay.api_path",
        default="/api.php",
        label="易支付查单路径",
        description="不同易支付系统可能是 /api.php 或 /xpay/epay/api.php。",
        value_type="text",
        min_value=1,
        max_value=160,
        unit="",
        group="payment",
    ),
    "payment.epay.submit_path": SettingSpec(
        key="payment.epay.submit_path",
        default="/submit.php",
        label="EPay submit path",
        description="Redirect payment path. Usually /submit.php or /xpay/epay/submit.php.",
        value_type="text",
        min_value=1,
        max_value=160,
        unit="",
        group="payment",
    ),
    "payment.epay.create_mode": SettingSpec(
        key="payment.epay.create_mode",
        default="redirect",
        label="EPay create mode",
        description="redirect returns a signed payment URL immediately; mapi waits for provider JSON.",
        value_type="text",
        min_value=4,
        max_value=16,
        unit="",
        group="payment",
    ),
    "payment.epay.timeout": SettingSpec(
        key="payment.epay.timeout",
        default=15,
        label="易支付请求超时",
        description="请求易支付接口的超时时间。",
        value_type="integer",
        min_value=3,
        max_value=60,
        unit="秒",
        group="payment",
    ),
    "promotion.invite_code_edit_enabled": SettingSpec(
        key="promotion.invite_code_edit_enabled",
        default=True,
        label="允许自定义推广码",
        description="关闭后用户不能修改自己的推广码，已有推广码和绑定关系继续有效。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="promotion",
    ),
    "promotion.settlement_check_interval_minutes": SettingSpec(
        key="promotion.settlement_check_interval_minutes",
        default=5,
        label="奖励释放扫描频率",
        description="后台扫描冻结代理收益和普通推广奖励的频率。",
        value_type="integer",
        min_value=1,
        max_value=120,
        unit="分钟",
        group="promotion",
    ),
    "promotion.min_agent_balance_for_subsidy_coupon_cents": SettingSpec(
        key="promotion.min_agent_balance_for_subsidy_coupon_cents",
        default=12800,
        label="代理补贴券最低余额门槛",
        description="代理创建可能低于成本的折扣券时，要求的最低代理余额。",
        value_type="integer",
        min_value=0,
        max_value=1_000_000,
        unit="分",
        group="promotion",
    ),
    "notification.email.enabled": SettingSpec(
        key="notification.email.enabled",
        default=False,
        label="启用邮件发送",
        description="关闭后注册、登录验证码和业务通知邮件将无法发送。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="notification_email",
    ),
    "notification.email.provider": SettingSpec(
        key="notification.email.provider",
        default="mode.email.aliyunemail.provider:create_provider",
        label="邮件插件路径",
        description="格式为 python.module.path:factory_name，默认使用阿里云邮件 SMTP 插件。",
        value_type="text",
        min_value=8,
        max_value=200,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_host": SettingSpec(
        key="notification.email.smtp_host",
        default="",
        label="SMTP 服务器",
        description="邮件服务 SMTP 地址，例如 smtpdm.aliyun.com。",
        value_type="text",
        min_value=0,
        max_value=120,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_port": SettingSpec(
        key="notification.email.smtp_port",
        default=465,
        label="SMTP 端口",
        description="SMTP SSL 通常为 465。",
        value_type="integer",
        min_value=1,
        max_value=65535,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_user": SettingSpec(
        key="notification.email.smtp_user",
        default="",
        label="SMTP 用户名",
        description="邮件服务登录用户名。",
        value_type="text",
        min_value=0,
        max_value=200,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_password": SettingSpec(
        key="notification.email.smtp_password",
        default="",
        label="SMTP 密码",
        description="邮件服务登录密码或授权码。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_from_email": SettingSpec(
        key="notification.email.smtp_from_email",
        default="",
        label="发件邮箱",
        description="用户收到邮件时看到的发件邮箱。",
        value_type="email",
        min_value=0,
        max_value=200,
        unit="",
        group="notification_email",
    ),
    "notification.email.smtp_from_name": SettingSpec(
        key="notification.email.smtp_from_name",
        default="QingYun Cloud",
        label="发件人名称",
        description="用户收到邮件时看到的发件人名称。",
        value_type="text",
        min_value=1,
        max_value=80,
        unit="",
        group="notification_email",
    ),
    "notification.sms.enabled": SettingSpec(
        key="notification.sms.enabled",
        default=False,
        label="启用短信发送",
        description="关闭后手机注册、手机登录和手机绑定验证码将无法发送。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.provider": SettingSpec(
        key="notification.sms.provider",
        default="mode.sms.aliyunsms.provider:create_provider",
        label="短信插件路径",
        description="格式为 python.module.path:factory_name，默认使用阿里云短信插件。",
        value_type="text",
        min_value=8,
        max_value=200,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.access_key_id": SettingSpec(
        key="notification.sms.access_key_id",
        default="",
        label="AccessKey ID",
        description="短信服务 AccessKey ID。",
        value_type="text",
        min_value=0,
        max_value=200,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.access_key_secret": SettingSpec(
        key="notification.sms.access_key_secret",
        default="",
        label="AccessKey Secret",
        description="短信服务 AccessKey Secret。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.endpoint": SettingSpec(
        key="notification.sms.endpoint",
        default="dypnsapi.aliyuncs.com",
        label="短信 Endpoint",
        description="阿里云号码认证服务 endpoint。",
        value_type="text",
        min_value=1,
        max_value=120,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.sign_name": SettingSpec(
        key="notification.sms.sign_name",
        default="",
        label="短信签名",
        description="已审核通过的短信签名。",
        value_type="text",
        min_value=0,
        max_value=80,
        unit="",
        group="notification_sms",
    ),
    "notification.sms.template_code": SettingSpec(
        key="notification.sms.template_code",
        default="",
        label="默认模板 Code",
        description="未知用途时使用的短信模板；注册/登录/绑定默认已有内置模板映射。",
        value_type="text",
        min_value=0,
        max_value=80,
        unit="",
        group="notification_sms",
    ),
    "verification.enabled": SettingSpec(
        key="verification.enabled",
        default=False,
        label="启用实名认证插件",
        description="关闭后用户不能发起新的第三方实名认证。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="verification",
    ),
    "verification.provider": SettingSpec(
        key="verification.provider",
        default="mode.verification.baidu_face_v3.provider:create_provider",
        label="实名认证插件路径",
        description="格式为 python.module.path:factory_name，默认使用百度智能云实名认证 V3 插件。",
        value_type="text",
        min_value=8,
        max_value=200,
        unit="",
        group="verification",
    ),
    "verification.default_mode": SettingSpec(
        key="verification.default_mode",
        default="two_factor",
        label="默认核验方式",
        description="用户端默认展示的实名认证方式：two_factor、three_factor 或 face_redirect；扫脸统一使用第三方 H5 跳转核身。",
        value_type="text",
        min_value=1,
        max_value=32,
        unit="",
        group="verification",
    ),
    "verification.retention_days": SettingSpec(
        key="verification.retention_days",
        default=3,
        label="材料保留天数",
        description="实名认证敏感材料的保留天数；后台清理任务约每 24 小时执行一次。",
        value_type="integer",
        min_value=1,
        max_value=30,
        unit="天",
        group="verification",
    ),
    "verification.free_quota_per_user": SettingSpec(
        key="verification.free_quota_per_user",
        default=1,
        label="每人免费实名次数",
        description="每个用户可免费发起实名认证的次数；按提交次数消耗，已实名用户不能继续提交。",
        value_type="integer",
        min_value=0,
        max_value=100,
        unit="次",
        group="verification",
    ),
    "verification.quota_purchase_enabled": SettingSpec(
        key="verification.quota_purchase_enabled",
        default=True,
        label="允许购买实名次数",
        description="免费次数用完后，允许用户用余额购买实名认证提交次数。",
        value_type="boolean",
        min_value=None,
        max_value=None,
        unit="",
        group="verification",
    ),
    "verification.quota_price_cents": SettingSpec(
        key="verification.quota_price_cents",
        default=350,
        label="实名次数单价",
        description="用户购买一次实名认证提交次数的价格，单位分；350 表示 ¥3.50。",
        value_type="integer",
        min_value=0,
        max_value=100000,
        unit="分",
        group="verification",
    ),
    "verification.baidu_face_v3.access_key": SettingSpec(
        key="verification.baidu_face_v3.access_key",
        default=app_settings.BAIDU_FACE_API_KEY,
        label="百度 API Key",
        description="百度智能云应用 API Key。",
        value_type="text",
        min_value=0,
        max_value=200,
        unit="",
        group="verification",
    ),
    "verification.baidu_face_v3.secret_key": SettingSpec(
        key="verification.baidu_face_v3.secret_key",
        default=app_settings.BAIDU_FACE_SECRET_KEY,
        label="百度 Secret Key",
        description="百度智能云应用 Secret Key。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="verification",
    ),
    "verification.aliyun_idcert.appcode": SettingSpec(
        key="verification.aliyun_idcert.appcode",
        default="",
        label="阿里云市场 AppCode",
        description="阿里云市场身份证二要素接口 AppCode。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="verification",
    ),
    "verification.aliyun_idcert.endpoint": SettingSpec(
        key="verification.aliyun_idcert.endpoint",
        default="https://idcert.market.alicloudapi.com/idcard",
        label="阿里云市场二要素接口地址",
        description="阿里云市场身份证实名认证接口调用地址。",
        value_type="url",
        min_value=8,
        max_value=200,
        unit="",
        group="verification",
    ),
    "verification.tencent.secret_id": SettingSpec(
        key="verification.tencent.secret_id",
        default="",
        label="腾讯云 SecretId",
        description="腾讯云访问密钥 SecretId。",
        value_type="text",
        min_value=0,
        max_value=200,
        unit="",
        group="verification",
    ),
    "verification.tencent.secret_key": SettingSpec(
        key="verification.tencent.secret_key",
        default="",
        label="腾讯云 SecretKey",
        description="腾讯云访问密钥 SecretKey。",
        value_type="password",
        min_value=0,
        max_value=500,
        unit="",
        group="verification",
    ),
    "verification.tencent.wechat_h5.rule_id": SettingSpec(
        key="verification.tencent.wechat_h5.rule_id",
        default="",
        label="腾讯云普通 H5 RuleId",
        description="腾讯云人脸核身普通 H5 流程 RuleId。",
        value_type="text",
        min_value=0,
        max_value=80,
        unit="",
        group="verification",
    ),
    "verification.tencent.wechat_float_h5.rule_id": SettingSpec(
        key="verification.tencent.wechat_float_h5.rule_id",
        default="",
        label="腾讯云浮层 H5 RuleId",
        description="腾讯云人脸核身微信浮层 H5 流程 RuleId。",
        value_type="text",
        min_value=0,
        max_value=80,
        unit="",
        group="verification",
    ),
    "verification.tencent.endpoint": SettingSpec(
        key="verification.tencent.endpoint",
        default="faceid.tencentcloudapi.com",
        label="腾讯云 Endpoint",
        description="腾讯云人脸核身 API Endpoint。",
        value_type="text",
        min_value=1,
        max_value=120,
        unit="",
        group="verification",
    ),
    "verification.tencent.info_type": SettingSpec(
        key="verification.tencent.info_type",
        default="1",
        label="腾讯云 InfoType",
        description="GetDetectInfoEnhanced 的 InfoType，默认只获取文字结果。",
        value_type="text",
        min_value=1,
        max_value=16,
        unit="",
        group="verification",
    ),
    "verification.tencent.score_threshold": SettingSpec(
        key="verification.tencent.score_threshold",
        default=80,
        label="腾讯云扫脸通过分数",
        description="腾讯云返回 Sim 时，大于等于该值判定为通过。",
        value_type="integer",
        min_value=0,
        max_value=100,
        unit="分",
        group="verification",
    ),
    "points.qq_answer": SettingSpec(
        key="points.qq_answer",
        default="2576462961",
        label="QQ群任务正确答案",
        description="用户完成加入QQ群任务时需填写的群主QQ号。",
        value_type="text",
        min_value=1,
        max_value=64,
        unit="",
        group="points",
    ),
    "points.bilibili_answer": SettingSpec(
        key="points.bilibili_answer",
        default="JSLASS",
        label="B站关注任务正确答案",
        description="用户完成关注B站任务时需填写的B站用户名。",
        value_type="text",
        min_value=1,
        max_value=64,
        unit="",
        group="points",
    ),
    "points.qq_reward": SettingSpec(
        key="points.qq_reward",
        default=1000,
        label="QQ群任务奖励积分",
        description="完成加入QQ群任务获得的积分。",
        value_type="integer",
        min_value=0,
        max_value=1_000_000,
        unit="积分",
        group="points",
    ),
    "points.bilibili_reward": SettingSpec(
        key="points.bilibili_reward",
        default=1000,
        label="B站关注任务奖励积分",
        description="完成关注B站任务获得的积分。",
        value_type="integer",
        min_value=0,
        max_value=1_000_000,
        unit="积分",
        group="points",
    ),
    "points.checkin_min_points": SettingSpec(
        key="points.checkin_min_points",
        default=100,
        label="每日签到最低积分",
        description="每日签到随机奖励的最小积分值。",
        value_type="integer",
        min_value=1,
        max_value=1_000_000,
        unit="积分",
        group="points",
    ),
    "points.checkin_max_points": SettingSpec(
        key="points.checkin_max_points",
        default=200,
        label="每日签到最高积分",
        description="每日签到随机奖励的最大积分值。",
        value_type="integer",
        min_value=1,
        max_value=1_000_000,
        unit="积分",
        group="points",
    ),
    "points.withdraw_fee_rate": SettingSpec(
        key="points.withdraw_fee_rate",
        default=200,
        label="积分提现手续费率",
        description="积分提现到余额的手续费率，单位为万分比，200 表示 2%。",
        value_type="integer",
        min_value=0,
        max_value=10000,
        unit="‱",
        group="points",
    ),
    "points.withdraw_min_points": SettingSpec(
        key="points.withdraw_min_points",
        default=25000,
        label="积分最低提现积分",
        description="积分提现的最低门槛，按积分计算。",
        value_type="integer",
        min_value=1,
        max_value=1_000_000_000,
        unit="积分",
        group="points",
    ),
}


@dataclass(frozen=True)
class BusinessSettings:
    full_refund_days: int
    full_refund_limit: int
    expire_suspend_after_days: int
    expire_delete_after_days: int
    task_sync_interval_seconds: int
    stock_sync_interval_minutes: int
    site_name: str
    registration_enabled: bool
    frontend_url: str
    backend_url: str
    promotion_base_url: str
    recharge_enabled: bool
    recharge_min_cents: int
    recharge_max_cents: int
    pending_payment_ttl_minutes: int
    invite_code_edit_enabled: bool
    settlement_check_interval_minutes: int
    min_agent_balance_for_subsidy_coupon_cents: int
    qq_answer: str
    bilibili_answer: str
    qq_reward: int
    bilibili_reward: int
    checkin_min_points: int
    checkin_max_points: int
    withdraw_fee_rate: int
    withdraw_min_points: int


@dataclass(frozen=True)
class EmailProviderConfig:
    email_enabled: bool
    EMAIL_PROVIDER: str
    SMTP_HOST: str
    SMTP_PORT: int
    SMTP_USER: str
    SMTP_PASSWORD: str
    SMTP_FROM_EMAIL: str
    SMTP_FROM_NAME: str


@dataclass(frozen=True)
class SmsProviderConfig:
    sms_enabled: bool
    SMS_PROVIDER: str
    SMS_ACCESS_KEY_ID: str
    SMS_ACCESS_KEY_SECRET: str
    SMS_ENDPOINT: str
    SMS_SIGN_NAME: str
    SMS_TEMPLATE_CODE: str


def _to_int(value: Any, default: int) -> int:
    if value in (None, ""):
        return default
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _coerce_value(spec: SettingSpec, value: Any) -> Any:
    if spec.value_type == "integer":
        coerced = _to_int(value, int(spec.default))
        min_value = spec.min_value if spec.min_value is not None else coerced
        max_value = spec.max_value if spec.max_value is not None else coerced
        return max(min_value, min(max_value, coerced))
    if spec.value_type == "boolean":
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            return value.strip().lower() in {"1", "true", "yes", "on", "enabled"}
        return bool(value)
    if spec.value_type in {"text", "url", "password", "email"}:
        text = str(value if value is not None else spec.default).strip()
        if spec.value_type == "url":
            text = text.rstrip("/")
            if not (text.startswith("http://") or text.startswith("https://")):
                text = str(spec.default).rstrip("/")
        if spec.value_type == "email" and text and ("@" not in text or "." not in text.rsplit("@", 1)[-1]):
            text = str(spec.default).strip()
        if spec.min_value is not None and len(text) < spec.min_value:
            text = str(spec.default).strip()
        if spec.max_value is not None:
            text = text[:spec.max_value]
        return text
    return value if value is not None else spec.default


def is_secret_setting_key(key: str, spec: SettingSpec | None = None) -> bool:
    text = str(key or "").lower()
    if spec and spec.value_type == "password":
        return True
    return any(marker in text for marker in ("password", "secret", "appcode")) or text.endswith(".access_key")


def is_masked_secret_value(value: Any) -> bool:
    return isinstance(value, str) and value == _MASK


def _decrypt_setting_value(key: str, value: Any, spec: SettingSpec | None = None) -> Any:
    if not is_secret_setting_key(key, spec):
        return value
    if value in (None, ""):
        return ""
    try:
        return decrypt_secret(str(value))
    except Exception:
        return value


def _is_encrypted_secret_value(value: Any) -> bool:
    if value in (None, ""):
        return False
    try:
        decrypt_secret(str(value))
        return True
    except Exception:
        return False


def _stored_setting_value(
    spec: SettingSpec,
    value: Any,
    *,
    current_value: Any = None,
) -> Any:
    if is_masked_secret_value(value) and current_value not in (None, ""):
        return current_value
    coerced = _coerce_value(spec, value)
    if not is_secret_setting_key(spec.key, spec):
        return coerced
    if coerced in (None, ""):
        return ""
    if _is_encrypted_secret_value(coerced):
        return coerced
    return encrypt_secret(str(coerced))


def _public_setting_value(item: SystemSetting, spec: SettingSpec) -> Any:
    value = _coerce_value(spec, _decrypt_setting_value(spec.key, item.value, spec))
    if is_secret_setting_key(spec.key, spec) and value not in (None, ""):
        return _MASK
    return value


def _validate_cross_settings(values: dict[str, Any]) -> None:
    suspend_days = int(values["lifecycle.expire_suspend_after_days"])
    delete_days = int(values["lifecycle.expire_delete_after_days"])
    if delete_days < suspend_days:
        raise HTTPException(status_code=400, detail="到期删除天数不能小于到期暂停天数")
    min_recharge = int(values["finance.recharge_min_cents"])
    max_recharge = int(values["finance.recharge_max_cents"])
    if max_recharge < min_recharge:
        raise HTTPException(status_code=400, detail="最高充值金额不能小于最低充值金额")
    checkin_min = int(values["points.checkin_min_points"])
    checkin_max = int(values["points.checkin_max_points"])
    if checkin_max < checkin_min:
        raise HTTPException(status_code=400, detail="每日签到最高积分不能小于最低积分")


async def ensure_system_settings(db: AsyncSession) -> dict[str, SystemSetting]:
    result = await db.execute(select(SystemSetting))
    settings = {item.key: item for item in result.scalars().all()}

    for spec in SETTING_SPECS.values():
        if spec.key in settings:
            item = settings[spec.key]
            if is_secret_setting_key(spec.key, spec) and item.value not in (None, "") and not _is_encrypted_secret_value(item.value):
                item.value = encrypt_secret(str(_coerce_value(spec, item.value)))
                item.label = spec.label
                item.description = spec.description
            continue
        item = SystemSetting(
            key=spec.key,
            value=_stored_setting_value(spec, spec.default),
            label=spec.label,
            description=spec.description,
        )
        db.add(item)
        settings[spec.key] = item

    await db.flush()
    return settings


async def get_business_settings(db: AsyncSession) -> BusinessSettings:
    settings = await ensure_system_settings(db)
    values = {
        key: _coerce_value(spec, settings.get(key).value if settings.get(key) else spec.default)
        for key, spec in SETTING_SPECS.items()
    }
    if values["lifecycle.expire_delete_after_days"] < values["lifecycle.expire_suspend_after_days"]:
        values["lifecycle.expire_delete_after_days"] = values["lifecycle.expire_suspend_after_days"]
    if values["finance.recharge_max_cents"] < values["finance.recharge_min_cents"]:
        values["finance.recharge_max_cents"] = values["finance.recharge_min_cents"]

    return BusinessSettings(
        full_refund_days=values["refund.full_refund_days"],
        full_refund_limit=values["refund.full_refund_limit"],
        expire_suspend_after_days=values["lifecycle.expire_suspend_after_days"],
        expire_delete_after_days=values["lifecycle.expire_delete_after_days"],
        task_sync_interval_seconds=values["upstream.task_sync_interval_seconds"],
        stock_sync_interval_minutes=values["upstream.stock_sync_interval_minutes"],
        site_name=values["platform.site_name"],
        registration_enabled=values["platform.registration_enabled"],
        frontend_url=values["platform.frontend_url"],
        backend_url=values["platform.backend_url"],
        promotion_base_url=values["platform.promotion_base_url"],
        recharge_enabled=values["finance.recharge_enabled"],
        recharge_min_cents=values["finance.recharge_min_cents"],
        recharge_max_cents=values["finance.recharge_max_cents"],
        pending_payment_ttl_minutes=values["finance.pending_payment_ttl_minutes"],
        invite_code_edit_enabled=values["promotion.invite_code_edit_enabled"],
        settlement_check_interval_minutes=values["promotion.settlement_check_interval_minutes"],
        min_agent_balance_for_subsidy_coupon_cents=values["promotion.min_agent_balance_for_subsidy_coupon_cents"],
        qq_answer=values["points.qq_answer"],
        bilibili_answer=values["points.bilibili_answer"],
        qq_reward=values["points.qq_reward"],
        bilibili_reward=values["points.bilibili_reward"],
        checkin_min_points=values["points.checkin_min_points"],
        checkin_max_points=values["points.checkin_max_points"],
        withdraw_fee_rate=values["points.withdraw_fee_rate"],
        withdraw_min_points=values["points.withdraw_min_points"],
    )


def setting_to_dict(item: SystemSetting, spec: SettingSpec) -> dict[str, Any]:
    value = _public_setting_value(item, spec)
    return {
        "key": spec.key,
        "value": value,
        "label": item.label or spec.label,
        "description": item.description or spec.description,
        "value_type": spec.value_type,
        "min_value": spec.min_value,
        "max_value": spec.max_value,
        "unit": spec.unit,
        "group": spec.group,
    }


async def list_business_settings(db: AsyncSession) -> list[dict[str, Any]]:
    settings = await ensure_system_settings(db)
    return [
        setting_to_dict(settings[spec.key], spec)
        for spec in SETTING_SPECS.values()
        if spec.key in _business_keys()
    ]


def _feature_keys() -> set[str]:
    verification_business_keys = {
        "verification.free_quota_per_user",
        "verification.quota_purchase_enabled",
        "verification.quota_price_cents",
    }
    return {
        key
        for key, spec in SETTING_SPECS.items()
        if spec.group in {"notification_email", "notification_sms", "payment"}
        or (spec.group == "verification" and key not in verification_business_keys)
    }


def _business_keys() -> set[str]:
    return set(SETTING_SPECS) - _feature_keys()


async def list_feature_settings(db: AsyncSession) -> list[dict[str, Any]]:
    settings = await ensure_system_settings(db)
    return [
        setting_to_dict(settings[spec.key], spec)
        for spec in SETTING_SPECS.values()
        if spec.key in _feature_keys()
    ]


def _coerced_current_values(settings: dict[str, SystemSetting]) -> dict[str, Any]:
    return {
        key: _coerce_value(
            spec,
            _decrypt_setting_value(key, settings.get(key).value if settings.get(key) else spec.default, spec),
        )
        for key, spec in SETTING_SPECS.items()
    }


async def get_email_provider_config(
    db: AsyncSession,
    overrides: dict[str, Any] | None = None,
) -> SimpleNamespace:
    settings = await ensure_system_settings(db)
    values = {key: _decrypt_setting_value(key, item.value, SETTING_SPECS.get(key)) for key, item in settings.items()}
    values.update(_coerced_current_values(settings))
    for key, value in (overrides or {}).items():
        if is_masked_secret_value(value) and is_secret_setting_key(key, SETTING_SPECS.get(key)):
            continue
        if key in SETTING_SPECS:
            values[key] = _coerce_value(SETTING_SPECS[key], value)
        elif key.startswith("notification.email."):
            values[key] = value
    data: dict[str, Any] = {
        "email_enabled": bool(values["notification.email.enabled"]),
        "EMAIL_PROVIDER": str(values["notification.email.provider"]),
        "SMTP_HOST": str(values["notification.email.smtp_host"]),
        "SMTP_PORT": int(values["notification.email.smtp_port"]),
        "SMTP_USER": str(values["notification.email.smtp_user"]),
        "SMTP_PASSWORD": str(values["notification.email.smtp_password"]),
        "SMTP_FROM_EMAIL": str(values["notification.email.smtp_from_email"]),
        "SMTP_FROM_NAME": str(values["notification.email.smtp_from_name"]),
    }
    for key, value in values.items():
        if key.startswith("notification.email."):
            attr = key.removeprefix("notification.email.").upper()
            data.setdefault(attr, value)
    return SimpleNamespace(**data)


async def get_sms_provider_config(
    db: AsyncSession,
    overrides: dict[str, Any] | None = None,
) -> SimpleNamespace:
    settings = await ensure_system_settings(db)
    values = {key: _decrypt_setting_value(key, item.value, SETTING_SPECS.get(key)) for key, item in settings.items()}
    values.update(_coerced_current_values(settings))
    for key, value in (overrides or {}).items():
        if is_masked_secret_value(value) and is_secret_setting_key(key, SETTING_SPECS.get(key)):
            continue
        if key in SETTING_SPECS:
            values[key] = _coerce_value(SETTING_SPECS[key], value)
        elif key.startswith("notification.sms."):
            values[key] = value
    data: dict[str, Any] = {
        "sms_enabled": bool(values["notification.sms.enabled"]),
        "SMS_PROVIDER": str(values["notification.sms.provider"]),
        "SMS_ACCESS_KEY_ID": str(values["notification.sms.access_key_id"]),
        "SMS_ACCESS_KEY_SECRET": str(values["notification.sms.access_key_secret"]),
        "SMS_ENDPOINT": str(values["notification.sms.endpoint"]),
        "SMS_SIGN_NAME": str(values["notification.sms.sign_name"]),
        "SMS_TEMPLATE_CODE": str(values["notification.sms.template_code"]),
    }
    for key, value in values.items():
        if key.startswith("notification.sms."):
            attr = key.removeprefix("notification.sms.").upper()
            data.setdefault(attr, value)
    return SimpleNamespace(**data)


async def get_verification_provider_config(
    db: AsyncSession,
    overrides: dict[str, Any] | None = None,
) -> SimpleNamespace:
    settings = await ensure_system_settings(db)
    values = {key: _decrypt_setting_value(key, item.value, SETTING_SPECS.get(key)) for key, item in settings.items()}
    values.update(_coerced_current_values(settings))
    for key, value in (overrides or {}).items():
        if is_masked_secret_value(value) and is_secret_setting_key(key, SETTING_SPECS.get(key)):
            continue
        if key in SETTING_SPECS:
            values[key] = _coerce_value(SETTING_SPECS[key], value)
        elif key.startswith("verification."):
            values[key] = value
    data: dict[str, Any] = {
        "verification_enabled": bool(values["verification.enabled"]),
        "VERIFICATION_PROVIDER": str(values["verification.provider"]),
        "DEFAULT_MODE": str(values["verification.default_mode"]),
        "RETENTION_DAYS": int(values["verification.retention_days"]),
        "ACCESS_KEY": str(values["verification.baidu_face_v3.access_key"]),
        "SECRET_KEY": str(values["verification.baidu_face_v3.secret_key"]),
        "SCORE_THRESHOLD": int(values.get("verification.baidu_face_v3.score_threshold", 80) or 80),
        "QUALITY_CONTROL": str(values.get("verification.baidu_face_v3.quality_control", "LOW") or "LOW"),
        "LIVENESS_CONTROL": str(values.get("verification.baidu_face_v3.liveness_control", "NORMAL") or "NORMAL"),
        "SPOOFING_CONTROL": str(values.get("verification.baidu_face_v3.spoofing_control", "NORMAL") or "NORMAL"),
    }
    for key, value in values.items():
        if key.startswith("verification."):
            attr = key.removeprefix("verification.").replace(".", "_").upper()
            data.setdefault(attr, value)
    return SimpleNamespace(**data)


def _split_csv(value: Any) -> list[str]:
    return [item.strip() for item in str(value or "").split(",") if item.strip()]


async def get_payment_provider_config(
    db: AsyncSession,
    overrides: dict[str, Any] | None = None,
) -> SimpleNamespace:
    settings = await ensure_system_settings(db)
    values = {key: _decrypt_setting_value(key, item.value, SETTING_SPECS.get(key)) for key, item in settings.items()}
    values.update(_coerced_current_values(settings))
    for key, value in (overrides or {}).items():
        if is_masked_secret_value(value) and is_secret_setting_key(key, SETTING_SPECS.get(key)):
            continue
        if key in SETTING_SPECS:
            values[key] = _coerce_value(SETTING_SPECS[key], value)
        elif key.startswith("payment."):
            values[key] = value

    data: dict[str, Any] = {
        "PAYMENT_ENABLED_PLUGINS": _split_csv(values["payment.enabled_plugins"]),
        "PAYMENT_EPAY_API_URL": str(values["payment.epay.api_url"]),
        "PAYMENT_EPAY_PID": str(values["payment.epay.pid"]),
        "PAYMENT_EPAY_KEY": str(values["payment.epay.key"]),
        "PAYMENT_EPAY_NOTIFY_URL": str(values["payment.epay.notify_url"]),
        "PAYMENT_EPAY_RETURN_URL": str(values["payment.epay.return_url"]),
        "PAYMENT_EPAY_MAPI_PATH": str(values["payment.epay.mapi_path"]),
        "PAYMENT_EPAY_API_PATH": str(values["payment.epay.api_path"]),
        "PAYMENT_EPAY_SUBMIT_PATH": str(values["payment.epay.submit_path"]),
        "PAYMENT_EPAY_CREATE_MODE": str(values["payment.epay.create_mode"]),
        "PAYMENT_EPAY_TIMEOUT": int(values["payment.epay.timeout"]),
        # Backward-compatible aliases used by the first generic EPay provider.
        "EPAY_API_URL": str(values["payment.epay.api_url"]),
        "EPAY_PID": str(values["payment.epay.pid"]),
        "EPAY_KEY": str(values["payment.epay.key"]),
        "EPAY_NOTIFY_URL": str(values["payment.epay.notify_url"]),
        "EPAY_RETURN_URL": str(values["payment.epay.return_url"]),
        "EPAY_SUBMIT_PATH": str(values["payment.epay.submit_path"]),
        "EPAY_CREATE_MODE": str(values["payment.epay.create_mode"]),
    }
    for key, value in values.items():
        if key.startswith("payment."):
            attr = key.removeprefix("payment.").replace(".", "_").upper()
            data.setdefault(attr, value)
    return SimpleNamespace(**data)


async def update_business_settings(db: AsyncSession, values: dict[str, Any]) -> list[dict[str, Any]]:
    allowed_keys = _business_keys()
    unknown_keys = sorted(set(values) - allowed_keys)
    if unknown_keys:
        raise HTTPException(status_code=400, detail=f"未知配置项：{', '.join(unknown_keys)}")

    settings = await ensure_system_settings(db)
    current = {
        key: _coerce_value(spec, _decrypt_setting_value(key, settings[key].value, spec))
        for key, spec in SETTING_SPECS.items()
    }
    for key, value in values.items():
        current[key] = _coerce_value(SETTING_SPECS[key], value)
    _validate_cross_settings(current)

    for key, value in values.items():
        spec = SETTING_SPECS[key]
        item = settings[key]
        item.value = _stored_setting_value(spec, value, current_value=item.value)
        item.label = spec.label
        item.description = spec.description

    await db.flush()
    return await list_business_settings(db)


async def update_feature_settings(db: AsyncSession, values: dict[str, Any]) -> list[dict[str, Any]]:
    allowed_keys = _feature_keys()
    unknown_keys = sorted(set(values) - allowed_keys)
    if unknown_keys:
        raise HTTPException(status_code=400, detail=f"未知功能配置项：{', '.join(unknown_keys)}")

    settings = await ensure_system_settings(db)
    for key, value in values.items():
        spec = SETTING_SPECS[key]
        item = settings[key]
        item.value = _stored_setting_value(spec, value, current_value=item.value)
        item.label = spec.label
        item.description = spec.description

    await db.flush()
    return await list_feature_settings(db)
