"""
广告智能决策引擎配置 — auto_put_ad_mini

运营可直接修改此文件调整决策参数。
当前模式：智能判断
  - 基于 动态 ROI (7日均值) 的精细化决策
  - AI 推理结合领域知识
  - 三级分类：零消耗待关停（规则）+ 待优化评估（智能）+ 正常运行（规则）
"""
import os
import logging
from datetime import date, datetime
from pathlib import Path
from typing import Optional
from zoneinfo import ZoneInfo
from agent.core.runner import RunConfig, KnowledgeConfig

# 初始化 logger（必须在使用前定义）
logger = logging.getLogger(__name__)

# 加载 .env 文件（如果存在）
try:
    from dotenv import load_dotenv
    load_dotenv(Path(__file__).parent / ".env")
except ImportError:
    pass

# ═══════════════════════════════════════════
# Agent 运行配置
# ═══════════════════════════════════════════

MAIN_CONFIG = RunConfig(
    model="anthropic/claude-sonnet-4.5",
    temperature=0.3,
    max_iterations=50,
    name="广告智能调控助手",
    tools=[
        "fetch_creative_data",
        "merge_creative_data",
        "calculate_roi_metrics",
        "calculate_creative_roi",      # 创意级动态 ROI(广告级 pause 候选的二次细化)
        "calculate_portfolio_summary",
        "get_ads_for_review",
        "apply_decisions",
        "query_ad_detail",      # Mode 2: 查询广告详情
        "modify_decisions",     # Mode 3: 修改已有决策
        "validate_decisions",
        "generate_report",
        # 执行引擎 + IM 审批（已集成阻塞式审批流）：
        "execute_decisions",
        "check_execution_feedback",
        "send_approval_request",
        "check_approval_status",
        "send_feishu_text_message",  # 执行后向您同步 diff / 确认 / 质疑回应
        # 飞书文档（报告导入 & 分享）：
        "import_to_feishu",
        # 注：曾考虑用内置 "agent" 工具按 tier 并行委托子 Agent，
        # 但框架的 agent 工具只返回文本 summary，主 Agent 拿不回结构化决策，
        # 会陷入"无法 apply"的死循环。直接在主 Agent 单次输出完成全部 decisions 更可靠。
    ],
    skills=[
        "ad-domain",           # 业务模型：裂变模型、R值、ROI公式、字段定义
        "platform-rules",      # 平台硬约束：oCPM学习期、调价上限、数据口径
        "decision-strategy",   # 决策策略：角色 + 基准 + 候选标记 + 年龄策略 + 7种action + 输出规范
        "posterior-wisdom",    # 后验经验：学习中断/降价恢复/创意冷启动/置信度分级
    ],
    extra_llm_params={"max_tokens": 32000},
    knowledge=KnowledgeConfig(
        enable_extraction=False,              # 从决策过程中提取后验经验（投放后开启）
        enable_completion_extraction=False,    # 完成后总结本轮经验（投放后开启）
        enable_injection=False,               # 决策时自动注入相关历史经验（投放后开启）
        owner="ad_mini_team",
    ),
)

SKILLS_DIR = str(Path(__file__).parent / "skills")
TRACE_STORE_PATH = ".trace"
LOG_LEVEL = "INFO"

# ═══════════════════════════════════════════
# 时区配置
# ═══════════════════════════════════════════
TIMEZONE = os.getenv("TZ", "Asia/Shanghai")
logger.info(f"运行时区：{TIMEZONE}")


def now_in_timezone() -> datetime:
    """返回项目统一业务时区的 aware datetime。"""
    return datetime.now(ZoneInfo(TIMEZONE))

# ═══════════════════════════════════════════
# V3 数据窗口配置
# ═══════════════════════════════════════════
DATA_WINDOW_DAYS = 14  # 数据采集窗口：14 天历史数据
ROI_CALCULATION_DAYS = 7  # 动态 ROI (7日均值) 计算窗口（保持 7 天）

# ═══════════════════════════════════════════
# V3 决策阈值（默认值，可被 SKILL 覆盖）
# ═══════════════════════════════════════════
MIN_DAILY_COST = 100  # 日消耗 >= 100元才参与 ROI 计算
MIN_AD_AGE_DAYS = 3  # 广告创建 >= 3天才参与决策（与 min_periods 对齐）
ROI_LOW_FACTOR = 0.75  # 动态 ROI (7日均值) < 全体均值 × 0.75 → 关停
NO_SPEND_THRESHOLD = 10  # 7日消耗均值 < 10元 → 关停
STABLE_SPEND_THRESHOLD = 100  # 稳定消耗定义：>100元/天

# ═══════════════════════════════════════════
# 出价调整配置
# ═══════════════════════════════════════════
BID_ADJUSTMENT_ENABLED = True
BID_DOWN_ROI_FACTOR = 0.90      # ROI < 均值×0.90 → 考虑降价（低于渠道均值10%）
BID_UP_ROI_FACTOR = 1.05        # ROI > 均值×1.05 → 考虑提价（高于渠道均值5%）
BID_UP_MAX_SPEND = 1000          # 提价消耗上限：均值消耗<1000才提价（投手经验原文）
BID_CHANGE_MIN_PCT = 0.03       # 最小调幅 3%（兼容旧代码）
BID_CHANGE_MAX_PCT = 0.10       # 最大单次调幅 10%（兼容旧代码）
BID_UP_MIN_PCT = 0.05           # 提价最小幅度 5%
BID_UP_MAX_PCT = 0.10           # 提价最大幅度 10%
BID_DOWN_MIN_PCT = 0.03         # 降价最小幅度 3%
BID_DOWN_MAX_PCT = 0.05         # 降价最大幅度 5%
BID_DOWN_MIN_SPEND = 500        # 降价消耗门槛：7日日均消耗≥500元
BID_FLOOR_YUAN = 0.05           # 出价下限（元）
BID_CEILING_YUAN = 1.00         # 出价上限（元）

# 广告年龄分段（基于决策树图片）
COLD_START_DAYS = 3             # 冷启动期（≤3天）：极度保护，几乎不干预
EARLY_GROWTH_DAYS = 7           # 早期成长期（4-7天）：可提价放量（满足ROI+消耗条件）
AD_AGE_MATURE = 7               # 成熟期（>7天）：全面调控

# 兼容性（已废弃）
AD_AGE_NEWBORN = COLD_START_DAYS   # 兼容旧代码
CAUTIOUS_DAYS = EARLY_GROWTH_DAYS  # 兼容旧代码

# 高燃烧预警配置
HIGH_BURN_AGE_THRESHOLD = 3     # 广告年龄>3天才检查
HIGH_BURN_COST_THRESHOLD = 300  # 昨日消耗>300元触发预警
ROI_LOW_MIN_YESTERDAY_COST = 300  # 关停消耗门槛：昨日消耗≥300才检查关停（投手经验2.4）

# ═══════════════════════════════════════════
# 创意级 pause 细化配置
# ═══════════════════════════════════════════
# 当广告级判 pause 时,先做创意级二次分析:全员低于阈值才真关广告,部分拖累只关差创意
CREATIVE_PAUSE_ENABLED = True       # 总开关:False 时全部走广告级 pause(降级路径)
CREATIVE_MIN_COST_SHARE = 0.15      # 创意 7 日消耗占比 < 此值视为数据稀疏,不纳入"判死刑"
CREATIVE_MIN_AGE_DAYS = 7           # 创意年龄 < 此值视为冷启动,不纳入"判死刑"(对齐广告级 EARLY_GROWTH_DAYS)
CREATIVE_MIN_VALID_DAYS = 3         # 创意有效 ROI 数据天数 < 此值视为不充分,不纳入"判死刑"
CREATIVE_MIN_REMAINING = 2          # 关停后剩余 eligible 创意数 < 此值,升级为广告级 pause
CREATIVE_MAX_PAUSE_COST_SHARE = 0.80 # pause_targets 总占消耗 > 此值,本质是关广告,升级为广告级 pause
CREATIVE_RATELIMIT_DAYS = 7         # 同一创意 7 天内不允许重复 pause

# ═══════════════════════════════════════════
# 安全护栏配置
# ═══════════════════════════════════════════
GUARDRAILS_ENABLED = True
DRY_RUN_MODE = False                       # 关闭干运行，让护栏正常放行（实际执行由 EXECUTION_ENABLED 控制）
MAX_ADJUSTMENTS_PER_AD_PER_DAY = 2
MIN_ADJUSTMENT_INTERVAL_HOURS = 6
MAX_DAILY_CUMULATIVE_CHANGE_PCT = 0.20     # 日累计调幅上限 20%
MAX_DAILY_OPS = 10000                       # 单日最多操作广告数（实际不限制）
DATA_FRESHNESS_MAX_HOURS = 96               # 数据超过 96 小时视为过期（已从48小时放宽至96小时）

# ═══════════════════════════════════════════
# 执行引擎配置
# ═══════════════════════════════════════════

# 执行开关（优先级：数据库 > 环境变量 > 默认值False）
EXECUTION_ENABLED = False
try:
    from db import get_system_config
    _db_execution_enabled = get_system_config("execution_enabled", default=None)
    if _db_execution_enabled is not None:
        EXECUTION_ENABLED = _db_execution_enabled
        logger.info(f"✅ 从数据库读取执行开关：{EXECUTION_ENABLED}")
    else:
        # 降级到环境变量
        _env_execution_enabled = os.getenv("EXECUTION_ENABLED", "").strip().lower()
        if _env_execution_enabled:
            EXECUTION_ENABLED = _env_execution_enabled in ("true", "1", "yes")
            logger.info(f"从环境变量读取执行开关：{EXECUTION_ENABLED}")
except Exception as e:
    logger.warning(f"⚠️ 数据库读取执行开关失败（{e}），使用默认值：{EXECUTION_ENABLED}")

API_QPS_LIMIT = 8                  # 保守QPS（平台上限10）
API_MAX_RETRIES = 3
TIER1_MAX_CHANGE_PCT = 0.00       # Tier1自动执行已禁用（改为0%，所有操作都需审批）
TIER3_MIN_DAILY_SPEND = 1500      # 高价值广告门槛（元/天）
FEEDBACK_CHECK_HOURS = 6

# ═══════════════════════════════════════════
# IM 审批配置（飞书直连）
# ═══════════════════════════════════════════
# 环境变量 IM_ENABLED 优先（"1"/"true"/"yes"=启用，"0"/"false"/"no"=禁用）
IM_ENABLED = os.getenv("IM_ENABLED", "true").strip().lower() not in {"0", "false", "no"}
IM_APPROVAL_TIMEOUT_MINUTES = 120  # 审批超时（分钟）— 2小时
IM_APPROVAL_POLL_INTERVAL_SECONDS = 30  # 审批轮询间隔（秒）

# 飞书应用凭据（"增长投放"机器人）— 优先从环境变量读取
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a955e97067f85cb3")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "NQaG4ci1plXRDTgwCqrLJgMLLoA2tdF8")

# 运营审批人飞书信息
FEISHU_OPERATOR_OPEN_ID = os.getenv("FEISHU_OPERATOR_OPEN_ID", "ou_498988d823b61ab89c9afe4310f85bb4")
FEISHU_OPERATOR_CHAT_ID = os.getenv("FEISHU_OPERATOR_CHAT_ID", "oc_88e0a1970a7de02eb5ac225a8b0cedea")
# 广告/创意创建专用群；未配置时兼容回退运营审批群。
FEISHU_CREATION_CHAT_ID = (
    os.getenv("FEISHU_CREATION_CHAT_ID", "").strip()
    or FEISHU_OPERATOR_CHAT_ID
)

# 投放项目群聊 — 用于接收决策结果通知和审批回复
# 置空则不发送到群，仅发送到个人
FEISHU_AD_PROJECT_CHAT_ID = os.getenv("FEISHU_AD_PROJECT_CHAT_ID", "oc_7940ec97cde40b245cff9cb606ff1ac7")

# 腾讯广告默认账户（测试账户）
TENCENT_AD_ACCOUNT_ID = int(os.getenv("TENCENT_AD_ACCOUNT_ID", "80769799"))

# ═══════════════════════════════════════════
# 账户白名单配置
# ═══════════════════════════════════════════

# 白名单模式开关（优先级：数据库 > 环境变量）
WHITELIST_ENABLED = None
WHITELIST_ACCOUNTS = []

# 尝试从数据库读取配置
try:
    from db import get_whitelist_accounts, get_system_config

    # 读取白名单开关
    WHITELIST_ENABLED = get_system_config("whitelist_enabled", default=None)

    # 读取白名单账户列表
    WHITELIST_ACCOUNTS = get_whitelist_accounts()

    logger.info(f"✅ 从数据库读取白名单配置：{len(WHITELIST_ACCOUNTS)} 个账户")
except Exception as db_error:
    logger.warning(f"⚠️ 数据库读取失败（{db_error}），降级到环境变量配置")

    # 降级方案1：从环境变量读取
    _whitelist_str = os.getenv("WHITELIST_ACCOUNTS", "")
    if _whitelist_str:
        # 格式：逗号分隔，如 "80769799,71305011"
        WHITELIST_ACCOUNTS = [int(x.strip()) for x in _whitelist_str.split(",") if x.strip()]
        logger.info(f"从环境变量读取白名单：{len(WHITELIST_ACCOUNTS)} 个账户")
    else:
        # 降级方案2：从文件读取（可选）
        _whitelist_file = Path(__file__).parent / "whitelist.json"
        if _whitelist_file.exists():
            import json
            with open(_whitelist_file) as f:
                whitelist_data = json.load(f)
                WHITELIST_ACCOUNTS = whitelist_data.get("accounts", [])
                logger.info(f"从 whitelist.json 读取白名单：{len(WHITELIST_ACCOUNTS)} 个账户")

# 白名单开关降级处理
if WHITELIST_ENABLED is None:
    WHITELIST_ENABLED = os.getenv("WHITELIST_ENABLED", "true").lower() == "true"

# 向后兼容：单账户模式
if not WHITELIST_ACCOUNTS:
    WHITELIST_ACCOUNTS = [TENCENT_AD_ACCOUNT_ID]
    logger.info(f"白名单为空，使用单账户模式：{TENCENT_AD_ACCOUNT_ID}")

logger.info(
    f"白名单配置：{'启用' if WHITELIST_ENABLED else '禁用'}，"
    f"账户数={len(WHITELIST_ACCOUNTS)}，列表={WHITELIST_ACCOUNTS[:5]}..."
)

# ═══════════════════════════════════════════
# 实验范围 Scope(MVP)
# ═══════════════════════════════════════════
# 2026-06-08:DB 白名单已直接收窄到 2 个测试账户,不再 override
# 单一真相源 = DB account_whitelist 表 enabled=1 的行
# 若要临时收窄,改这里为 {account_id...}
EXPERIMENTAL_SCOPE_ACCOUNTS = None
EXPERIMENTAL_SCOPE_REASON = "已迁 DB,config 不再 override"

if EXPERIMENTAL_SCOPE_ACCOUNTS is not None:
    _scope_before = len(WHITELIST_ACCOUNTS)
    WHITELIST_ACCOUNTS = [a for a in WHITELIST_ACCOUNTS if a in EXPERIMENTAL_SCOPE_ACCOUNTS]
    # 兜底:如 scope 中账户不在 DB 白名单(或 DB 读取失败),直接采用 scope 列表
    if not WHITELIST_ACCOUNTS:
        WHITELIST_ACCOUNTS = list(EXPERIMENTAL_SCOPE_ACCOUNTS)
        logger.warning(
            f"⚠️ EXPERIMENTAL_SCOPE {EXPERIMENTAL_SCOPE_ACCOUNTS} 与现有白名单交集为空，"
            f"直接使用 scope 列表"
        )
    logger.info(
        f"🧪 EXPERIMENTAL_SCOPE 启用：WHITELIST_ACCOUNTS 由 {_scope_before} → "
        f"{len(WHITELIST_ACCOUNTS)} 个 ({WHITELIST_ACCOUNTS})。原因：{EXPERIMENTAL_SCOPE_REASON}"
    )

# ═══════════════════════════════════════════
# 待投放账户配置（DB 单一真相源）
# ═══════════════════════════════════════════
# 人工只配置:account_id / audience_name / bid_min_fen / bid_max_fen / age_min / age_max / delivery_version。
# 系统自动解析 audience_pack_id、授权并验证目标账户可见后再创建广告。
# - audience_name="泛人群" 表示不使用 custom_audience,无需授权。
# - bid_amount_fen 若为空,按 bid_min_fen/bid_max_fen 和 BID_PICK_STRATEGY 取值。
def get_account_creation_config(account_id: int) -> dict:
    """从 DB 读取账户级广告创建配置。

    Returns:
        {
          account_id, audience_name, audience_pack_id,
          audience_tier_label, bid_amount_fen,
          bid_min_fen, bid_max_fen, age, delivery_version, ...
        }

    Raises:
        ValueError: 账户未配置、未启用,或缺少必要人工配置。
    """
    try:
        from db.connection import get_connection
    except Exception as e:
        raise ValueError(
            f"无法读取 account_id {account_id} 的创建配置: DB 模块不可用({e})"
        ) from e

    conn = get_connection()
    try:
        try:
            from configure_creation_accounts import ensure_account_delivery_config_columns
            ensure_account_delivery_config_columns()
        except Exception as e:
            logger.warning("确保账户投放配置列失败:%s", e)
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT c.account_id, c.enabled, c.delivery_version,
                       c.audience_name, c.audience_pack_id,
                       c.audience_tier_label, c.bid_min_fen, c.bid_max_fen,
                       c.bid_amount_fen, c.bid_scene, c.custom_cost_cap_fen,
                       c.automatic_site_enabled, c.site_set_json AS account_site_set_json,
                       c.location_types_json AS account_location_types_json,
                       c.region_ids_json AS account_region_ids_json,
                       c.age_min, c.age_max,
                       c.config_date,
                       c.daily_budget_fen AS account_daily_budget_fen,
                       t.site_set_json, t.location_types_json, t.region_ids_json,
                       t.daily_budget_fen, t.time_series_json,
                       w.enabled AS whitelist_enabled
                FROM ad_creation_account_config c
                JOIN account_whitelist w ON w.account_id = c.account_id
                JOIN ad_delivery_template t
                  ON t.delivery_version = c.delivery_version
                 AND t.enabled = TRUE
                WHERE c.account_id=%s
                """,
                (account_id,),
            )
            row = cur.fetchone()
    finally:
        conn.close()

    if not row:
        raise ValueError(
            f"account_id {account_id} 未写入 ad_creation_account_config,不能创建广告"
        )
    if not row.get("whitelist_enabled"):
        raise ValueError(
            f"account_id {account_id} 未在 account_whitelist 启用,不能创建广告"
        )
    if not row.get("enabled"):
        raise ValueError(
            f"account_id {account_id} 在 ad_creation_account_config 中未启用,不能创建广告"
        )

    audience_name = (row.get("audience_name") or "").strip()
    if not audience_name:
        raise ValueError(
            f"account_id {account_id} 缺少 audience_name。"
            "请先人工配置待投放人群包名称;泛人群账户写 audience_name='泛人群'"
        )

    bid_amount_fen = row.get("bid_amount_fen")
    bid_min_fen = row.get("bid_min_fen")
    bid_max_fen = row.get("bid_max_fen")
    if bid_amount_fen is None:
        if bid_min_fen is None or bid_max_fen is None:
            raise ValueError(
                f"account_id {account_id} 缺少 bid_amount_fen 或 bid_min_fen/bid_max_fen"
            )
        if int(bid_min_fen) > int(bid_max_fen):
            raise ValueError(
                f"account_id {account_id} 出价范围非法:"
                f" bid_min_fen={bid_min_fen} > bid_max_fen={bid_max_fen}"
            )
        if BID_PICK_STRATEGY == "max":
            bid_amount_fen = int(bid_max_fen)
        elif BID_PICK_STRATEGY == "min":
            bid_amount_fen = int(bid_min_fen)
        else:
            bid_amount_fen = int(round((int(bid_min_fen) + int(bid_max_fen)) / 2))

    pack_id = row.get("audience_pack_id")
    age_min = row.get("age_min")
    age_max = row.get("age_max")
    if age_min is None or age_max is None:
        raise ValueError(
            f"account_id {account_id} 缺少 age_min/age_max"
        )
    if int(age_min) > int(age_max):
        raise ValueError(
            f"account_id {account_id} 年龄范围非法: age_min={age_min} > age_max={age_max}"
        )

    import json as _json
    from tools.delivery_config import parse_bid_scene
    site_set = _json.loads(row.get("account_site_set_json") or row["site_set_json"])
    location_types = _json.loads(
        row["account_location_types_json"]
        if row.get("account_location_types_json") is not None
        else row["location_types_json"]
    )
    region_ids = _json.loads(
        row["account_region_ids_json"]
        if row.get("account_region_ids_json") is not None
        else row["region_ids_json"]
    )
    time_series = _json.loads(row["time_series_json"])
    automatic_site_enabled = row.get("automatic_site_enabled")

    return {
        "account_id": int(row["account_id"]),
        "delivery_version": row["delivery_version"],
        "audience_name": audience_name,
        "audience_pack_id": int(pack_id) if pack_id is not None else None,
        "audience_tier_label": row.get("audience_tier_label") or audience_name,
        "bid_min_fen": int(bid_min_fen) if bid_min_fen is not None else None,
        "bid_max_fen": int(bid_max_fen) if bid_max_fen is not None else None,
        "bid_amount_fen": int(bid_amount_fen),
        "bid_scene": parse_bid_scene(row.get("bid_scene")),
        "custom_cost_cap_fen": (
            int(row["custom_cost_cap_fen"])
            if row.get("custom_cost_cap_fen") is not None else None
        ),
        "age": [{"min": int(age_min), "max": int(age_max)}],
        "site_set": site_set,
        "automatic_site_enabled": bool(automatic_site_enabled) if automatic_site_enabled is not None else False,
        "location_types": location_types,
        "region_ids": region_ids,
        "config_date": (
            row["config_date"].isoformat()
            if row.get("config_date") is not None else None
        ),
        "daily_budget_fen": int(
            row["account_daily_budget_fen"]
            if row.get("account_daily_budget_fen") is not None
            else row["daily_budget_fen"]
        ),
        "time_series": time_series,
    }


def get_account_audience_pack(account_id: int) -> tuple[Optional[int], str]:
    """兼容旧调用:返回 (audience_pack_id, audience_tier_label)。"""
    cfg = get_account_creation_config(account_id)
    return cfg["audience_pack_id"], cfg["audience_tier_label"]


def get_creation_account_ids(config_date: Optional[date] = None) -> list[int]:
    """读取启用账户；显式日期仅用于历史复现，默认不限制配置日期。"""
    try:
        from db.connection import get_connection
    except Exception as e:
        logger.warning("读取待投放账户失败:DB 模块不可用(%s)", e)
        return []

    conn = get_connection()
    try:
        with conn.cursor() as cur:
            sql = """
                SELECT c.account_id
                FROM ad_creation_account_config c
                JOIN account_whitelist w ON w.account_id = c.account_id
                JOIN ad_delivery_template t
                  ON t.delivery_version = c.delivery_version
                 AND t.enabled = TRUE
                WHERE c.enabled = TRUE
                  AND w.enabled = TRUE
            """
            params: tuple = ()
            if config_date is not None:
                sql += " AND c.config_date = %s"
                params = (config_date,)
            sql += " ORDER BY c.id ASC"
            cur.execute(sql, params)
            rows = cur.fetchall()
            return [int(r["account_id"]) for r in rows]
    except Exception as e:
        logger.warning("读取待投放账户失败:%s", e)
        return []
    finally:
        conn.close()

# ═══════════════════════════════════════════════════════════════════
# [CREATION SOP] 广告搭建 SOP 固定参数(2026-06-05 业务确认)
# ═══════════════════════════════════════════════════════════════════
# 投放 SOP:广告搭建 = 固定定向 & 人群 & 出价
# 小程序产品:票圈 | 3亿人喜欢的视频平台
# 几乎所有维度都是固定的,LLM 不参与"营销内容/定向/出价类型"决策
# 唯一可变维度:site_set 组合(3 种) + 出价数值(从区间内取)

# --- 营销内容(全固定)---
MARKETING_GOAL = "MARKETING_GOAL_USER_GROWTH"
MARKETING_SUB_GOAL = "MARKETING_SUB_GOAL_UNKNOWN"
# 修正(2026-06-05 真实样本反推):marketing_carrier_type 是 JUMP_PAGE,不是 MINI_PROGRAM_WECHAT
# 小程序信息通过 marketing_asset_outer_spec 嵌套传递
MARKETING_CARRIER_TYPE = "MARKETING_CARRIER_TYPE_JUMP_PAGE"
MARKETING_CARRIER_NAME = "票圈 | 3亿人喜欢的视频平台"
MARKETING_CARRIER_GH_ID = "gh_ecd1ea0b84cf"          # 小程序 GH ID
MARKETING_CARRIER_WX_APP_ID = "wx89e7eb06478361d7"   # 小程序 WX AppID(可能 add 时不传,待 dry run)

# marketing_asset_outer_spec 嵌套结构(add 时传)
MARKETING_TARGET_TYPE = "MARKETING_TARGET_TYPE_MINI_PROGRAM_WECHAT"
MARKETING_ASSET_OUTER_SPEC = {
    "marketing_target_type": MARKETING_TARGET_TYPE,
    "marketing_asset_outer_id": MARKETING_CARRIER_GH_ID,
}
# conversion_id 不传(可选 + 不支持朋友圈版位)
# 改用 optimization_goal 直接走

# ═══════════════════════════════════════════════════════════════════
# 账户级 feedback_id 映射(2026-06-05 业务确认)
# ═══════════════════════════════════════════════════════════════════
# feedback_id = 监测链接 ID,是账户级配置,广告 add 时必填
# 短期:本字典占位;长期:迁到 DB(扩展 account_whitelist 表加列)
# 设计接口:get_account_feedback_id(account_id) → 先查 DB,fallback 本字典
ACCOUNT_FEEDBACK_ID_MAPPING = {
    # account_id : feedback_id
    83846793: 6700703,            # 用户 2026-06-05 提供(非人群包账户)
    83846804: 6700002,            # 用户 2026-06-05 提供(R330+ 人群包账户)
}


def get_account_feedback_id(account_id: int):
    """获取账户的 feedback_id(监测链接 ID)。

    优先级:
    1. DB account_whitelist.feedback_id
    2. ad_creation_account_config.feedback_key → feedback_asset_template 自动查找/创建
    3. config.py 旧字典

    返回 None 时调用方应反问或报错,不能猜测。
    """
    db_feedback_id = _get_account_feedback_id_from_db(account_id)
    if db_feedback_id is not None:
        return db_feedback_id

    try:
        created_id = _ensure_account_feedback_id(account_id)
        if created_id is not None:
            return created_id
    except Exception as e:
        logger.warning(
            "自动获取/创建 account_id %s 的 feedback_id 失败, fallback config 字典:%s",
            account_id, e,
        )
    return ACCOUNT_FEEDBACK_ID_MAPPING.get(account_id)


def _get_account_feedback_id_from_db(account_id: int) -> Optional[int]:
    """从 account_whitelist 读取账户级 feedback_id。"""
    try:
        from db.connection import get_connection
        conn = get_connection()
        try:
            with conn.cursor() as cur:
                cur.execute(
                    "SELECT feedback_id FROM account_whitelist WHERE account_id=%s",
                    (account_id,),
                )
                row = cur.fetchone()
                if row and row.get("feedback_id") is not None:
                    return int(row["feedback_id"])
        finally:
            conn.close()
    except Exception as e:
        logger.warning(
            "读取 account_id %s 的 DB feedback_id 失败, fallback config 字典:%s",
            account_id, e,
        )
    return None


def _ensure_account_feedback_id(account_id: int) -> Optional[int]:
    """按账户绑定的 feedback_key 查找或新建 DataNexus 监测链接组。

    feedback_url 是全局模板;feedback_id 是账户级结果,拿到后写回
    account_whitelist.feedback_id。
    """
    import json as _json
    import time as _time
    import uuid as _uuid
    from urllib.parse import urlencode as _urlencode

    import httpx as _httpx

    try:
        from db.connection import get_connection
    except Exception as e:
        raise RuntimeError(f"DB 模块不可用:{e}") from e

    conn = get_connection()
    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT f.feedback_key, f.feedback_name, f.second_category_type,
                       f.feedback_type, f.feedback_url
                FROM ad_creation_account_config c
                JOIN feedback_asset_template f
                  ON f.feedback_key = c.feedback_key
                 AND f.enabled = TRUE
                WHERE c.account_id=%s
                  AND c.enabled = TRUE
                """,
                (account_id,),
            )
            tpl = cur.fetchone()
    finally:
        conn.close()

    if not tpl:
        return None

    feedback_name = str(tpl["feedback_name"])
    second_category_type = str(tpl["second_category_type"])
    feedback_type = str(tpl["feedback_type"])
    feedback_url = str(tpl["feedback_url"])

    token = _get_datanexus_access_token(account_id)
    common = {
        "access_token": token,
        "timestamp": str(int(_time.time())),
        "nonce": _uuid.uuid4().hex,
    }

    page = 1
    while True:
        params = dict(common)
        params.update({"account_id": account_id, "page": page, "page_size": 100})
        resp = _httpx.get(
            f"https://api.e.qq.com/v3.0/feedback_info/get?{_urlencode(params)}",
            timeout=30,
        ).json()
        if resp.get("code") != 0:
            raise RuntimeError(
                f"feedback_info/get 失败 code={resp.get('code')} "
                f"msg={resp.get('message_cn') or resp.get('message')}"
            )
        data = resp.get("data") or {}
        items = data.get("list") or []
        for item in items:
            if item.get("second_category_type") != second_category_type:
                continue
            url_infos = item.get("url_info_list") or []
            for info in url_infos:
                if info.get("feedback_type") == feedback_type and info.get("feedback_url") == feedback_url:
                    feedback_id = int(item["feedback_id"])
                    _write_account_feedback_id(account_id, feedback_id)
                    logger.info(
                        "[feedback] account=%d 复用已有 feedback_id=%s name=%s",
                        account_id, feedback_id, item.get("feedback_name"),
                    )
                    return feedback_id
            if item.get("feedback_name") == feedback_name:
                feedback_id = int(item["feedback_id"])
                _write_account_feedback_id(account_id, feedback_id)
                logger.info(
                    "[feedback] account=%d 按名称复用已有 feedback_id=%s name=%s",
                    account_id, feedback_id, item.get("feedback_name"),
                )
                return feedback_id

        page_info = data.get("page_info") or resp.get("page_info") or {}
        total_page = int(page_info.get("total_page") or page)
        if page >= total_page:
            break
        page += 1

    add_params = dict(common)
    add_params["nonce"] = _uuid.uuid4().hex
    body = {
        "account_id": account_id,
        "feedback_name": feedback_name,
        "second_category_type": second_category_type,
        "url_info_list": [
            {
                "feedback_type": feedback_type,
                "feedback_url": feedback_url,
            }
        ],
    }
    add_resp = _httpx.post(
        f"https://api.e.qq.com/v3.0/feedback_info/add?{_urlencode(add_params)}",
        json=body,
        timeout=30,
    ).json()
    if add_resp.get("code") != 0:
        raise RuntimeError(
            f"feedback_info/add 失败 code={add_resp.get('code')} "
            f"msg={add_resp.get('message_cn') or add_resp.get('message')} "
            f"body={_json.dumps(body, ensure_ascii=False)[:300]}"
        )
    feedback_id = int(((add_resp.get("data") or {}).get("feedback_info") or {})["feedback_id"])
    _write_account_feedback_id(account_id, feedback_id)
    logger.info(
        "[feedback] account=%d 新建 feedback_id=%s feedback_key=%s",
        account_id, feedback_id, tpl.get("feedback_key"),
    )
    return feedback_id


def _get_datanexus_access_token(account_id: int) -> str:
    """获取 DataNexus API access_token。

    当前内部 token API 实测可用于 /feedback_info/get;如后续拆分 DataNexus
    专用 token,可通过 DATANEXUS_ACCESS_TOKEN 或 DATANEXUS_TOKEN_API 覆盖。
    """
    import httpx as _httpx

    static_token = os.getenv("DATANEXUS_ACCESS_TOKEN", "").strip()
    if static_token:
        return static_token

    token_api = os.getenv("DATANEXUS_TOKEN_API") or os.getenv(
        "TENCENT_AD_TOKEN_API",
        "https://api.piaoquantv.com/ad/put/tencent/getAccessToken",
    )
    resp = _httpx.get(token_api, params={"accountId": account_id}, timeout=10)
    resp.raise_for_status()
    token = resp.text.strip()
    if not token or len(token) <= 10:
        raise RuntimeError("DataNexus token 返回异常")
    return token


def _write_account_feedback_id(account_id: int, feedback_id: int) -> None:
    """把账户级 DataNexus feedback_id 写回白名单。"""
    from db.connection import get_connection

    conn = get_connection()
    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                UPDATE account_whitelist
                SET feedback_id=%s,
                    updated_by=%s,
                    updated_at=CURRENT_TIMESTAMP
                WHERE account_id=%s
                """,
                (feedback_id, "auto-feedback-init", account_id),
            )
        conn.commit()
    finally:
        conn.close()

OPTIMIZATION_GOAL = "OPTIMIZATIONGOAL_PROMOTION_VIEW_KEY_PAGE"   # 关键页面访问次数(USER_GROWTH 配套)
# 注:2026-06-05 业务确认 — 两个测试账户均走 USER_GROWTH + PAGE_KEY 路线
# 即使 83846804 带人群包,仍用此优化目标,不切换到 BRAND_PROMOTION + CLICK

# --- 定向(全固定 SOP)---
FIXED_TARGETING_AGE = [{"min": 45, "max": 66}]        # 45-66 岁(自定义)
FIXED_TARGETING_GENDER = "ALL"                        # 不限制(不传 gender)
FIXED_TARGETING_LOCATION_TYPES = ["LIVE_IN"]          # 常住地

# 地域 region_id 列表 — 从 JSON 读(运营可改 JSON 调整生效地域)
# 来源:ad 95205841163 (account 81214386) 实际投放地域反推
# 实际排除:港澳台 + 东三省 + 河南(共 7 个一级行政区)
FIXED_TARGETING_REGIONS_JSON_PATH = Path(__file__).parent / "data" / "tencent_constants" / "regions_sop_current.json"
try:
    import json as _json
    with open(FIXED_TARGETING_REGIONS_JSON_PATH, encoding="utf-8") as _f:
        _regions_data = _json.load(_f)
    FIXED_TARGETING_REGION_IDS = [r["id"] for r in _regions_data["list"]]
    logger.info(
        f"✅ 从 {FIXED_TARGETING_REGIONS_JSON_PATH.name} 加载 {len(FIXED_TARGETING_REGION_IDS)} 个地域 region_id"
    )
except FileNotFoundError:
    FIXED_TARGETING_REGION_IDS = []
    logger.warning(
        f"⚠️ 地域 JSON 未找到:{FIXED_TARGETING_REGIONS_JSON_PATH},新建广告时 targeting.geo_location 为空"
    )

# 实际排除的一级行政区(给审批表 / 报告人类可读用)
EXCLUDED_PROVINCES_SEMANTIC = ["香港", "澳门", "台湾", "辽宁", "吉林", "黑龙江", "河南"]

# --- 出价 / 计费 ---
BID_MODE = "BID_MODE_OCPM"
SMART_BID_TYPE = "SMART_BID_TYPE_CUSTOM"
BID_STRATEGY = "BID_STRATEGY_AVERAGE_COST"
# 一键起量(auto_acquisition)默认关闭(用户 2026-06-05 确认,与多数样本一致)
# 注:腾讯硬约束 — 若启用,budget 必须 >= 20000(200 元)
AUTO_ACQUISITION_ENABLED = False
AUTO_ACQUISITION_BUDGET_FEN = 20000                   # 占位最小值,enabled=False 时不生效
AUTO_DERIVED_CREATIVE_ENABLED = False
AIM_SMART_TARGETING_ENABLED = False
AIM_SMART_SITE_ENABLED = False
DEEP_CONVERSION_SPEC: dict = {}

# --- 转化(conversion_id)---
# 用户 2026-06-05 指示:两个测试账户都用 1007(与样本 92067863445 一致)
# 长期:迁到 account_whitelist 表加列 conversion_id
DEFAULT_CONVERSION_ID = 1007

# --- 搜索场景扩量 · 定向拓展开关(用户 2026-06-05 反推确认)---
# 3 条线上样本均为 CLOSE,我们若不传腾讯默认 OPEN → 与 SOP 不一致
# 用 ad_api 反推得知字段名:search_expand_targeting_switch
SEARCH_EXPAND_TARGETING_SWITCH = "SEARCH_EXPAND_TARGETING_SWITCH_CLOSE"

# --- 版位(2026-06-09 用户确认:参考 78420850/105832100128 加朋友圈版位)---
AVAILABLE_SITE_SETS = [
    "SITE_SET_WECHAT",                 # 微信公众号
    "SITE_SET_WECHAT_PLUGIN",          # 微信插件
    "SITE_SET_SEARCH_SCENE",           # 搜索场景
    "SITE_SET_MOMENTS",                # 朋友圈
]
# MVP 阶段:单一固定版位组合(差异化先不靠 site_set)
SITE_SET_COMBINATIONS = [
    ["SITE_SET_WECHAT", "SITE_SET_WECHAT_PLUGIN", "SITE_SET_SEARCH_SCENE", "SITE_SET_MOMENTS"],
]

# --- AIM 智能定向(2026-06-11 实测修正:跨接口字段名/枚举值不一致)---
# 文档:https://developers.e.qq.com/v3.0/docs/api/adgroups/update
#       https://developers.e.qq.com/v3.0/docs/enums#smart_targeting_mode
#
# write 接口(/adgroups/add + /adgroups/update)字段 = smart_targeting_mode (enum)
#   · SMART_TARGETING_MANUAL = 手动定向(等价于 AIM 关闭,本期目标)
#   · 不传该字段 = 腾讯默认开 AIM(实测:get 反查会变成 SMART_TARGETING_AUTO)
#   · 必须每次显式传(腾讯文档原话:"若选择手动定向需在每次调用接口时显式携带该参数")
#
# read 接口(/adgroups/get)字段 = smart_targeting_status (只读)
#   · SMART_TARGETING_NONE = 已关 · SMART_TARGETING_AUTO = 智能定向中
SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL"

# --- WECHAT_POSITION 定投场景(2026-07-06 用户确认:扩展公众号 + 小程序位置)---
# 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 补 1024797
# → 2026-07-06 扩展公众号文章/订阅号相关位置,并补发现小程序。
# 中文映射通过 tools.scene_spec.get_wechat_position_tags(account_id) 运行时查询(进程内缓存 1h)
#
# 业务生效场景(公众号内容场景):
#   1024789 公众号文章底部
#   1024790 公众号文章中部
#   1024791 公众号文章视频贴片
#   1024792 订阅号消息列表
#   2100802 公众号文章评论区
#
# 业务生效场景(小程序流量位):
#   1024795 小程序激励式广告
#   1024796 小程序插屏广告
#   1024797 小程序封面广告
#   2100745 发现小程序
#   2100748 小程序原生广告
#
# 1024794 小程序 banner 广告已移除:
#    → /adgroups/update 接口已下线,报 code=1800945
#    → 2026-06-30 /adgroups/add 对 84502339 同样报 code=1800945
#
# ⚠️ 创建后锁死(2026-06-11 实测复现):wechat_position 一旦创建,update 报 code=36840
#    → 要新增场景必须删除重建广告,代价是丢失已挂创意 + 学习数据
WECHAT_POSITION_TARGETED_PRESET = [
    # 公众号内容场景
    1024789, 1024790, 1024791, 1024792, 2100802,
    # 小程序场景
    1024795, 1024796, 1024797, 2100745, 2100748,
]

# 1 账户广告数(2026-06-09 用户确认:2 条,一条有 wechat_position 定投,一条无定投)
ADS_PER_ACCOUNT = 2

# --- 时段 / 日期(当前统一为每天 06:00-20:00)---
# 一天 48 段 × 7 天 = 336 位字符串
TIME_SERIES_ONE_DAY = "0" * 12 + "1" * 28 + "0" * 8
TIME_SERIES_DEFAULT = TIME_SERIES_ONE_DAY * 7
# 长度自验
assert len(TIME_SERIES_DEFAULT) == 336, f"time_series 长度错误:{len(TIME_SERIES_DEFAULT)}"

DEFAULT_BEGIN_DATE_OFFSET_DAYS = 0                    # 创建当日开始
DEFAULT_END_DATE = "0"                                # 真实样本是字符串 "0",不是 None(腾讯特殊表示长期)

# --- 预算(用户 2026-06-05 确认:200 元/广告)---
# 真实样本是 0(不限),但 MVP 阶段我们设硬上限保护
DEFAULT_DAILY_BUDGET_YUAN = 200                       # 单广告日预算
DEFAULT_DAILY_BUDGET_FEN = DEFAULT_DAILY_BUDGET_YUAN * 100   # 元 → 分

# --- 出价区间表(按 audience tier label 索引)---
# 来源:用户提供的投放 SOP 出价区间表
AUDIENCE_BID_RANGES = {
    # tier_label                : (min_yuan, max_yuan)
    "R50_泛惊奇_奇观技艺":         (0.35, 0.45),
    "R50_泛知识_生活科普":         (0.35, 0.45),
    "R50_泛知识_时政历史":         (0.35, 0.45),
    "R50_泛祝福":                 (0.35, 0.45),
    "R50_全品类":                 (0.25, 0.38),
    "R50_同感个体_个人情感":       (0.35, 0.45),
    "R50_同感个体_退休榜样":       (0.35, 0.45),
    "R500_全品类":                (0.38, 0.48),
    "回流100-180":                (0.22, 0.28),
    "回流180-330":                (0.30, 0.40),
    "回流330+":                   (0.35, 0.40),
    "R330+":                     (0.35, 0.40),         # 别名,83846804(Q-R_330+)用
    "回流50-100":                 (0.19, 0.22),
    "泛人群":                     (0.19, 0.25),
    "no_audience_pack":          (0.19, 0.25),         # 别名,83846793(无人群包)用
}

# --- 出价取值策略 ---
BID_PICK_STRATEGY = "midpoint"                        # midpoint / max / min / random
COLD_START_BID_PICK_STRATEGY = "midpoint"             # 冷启动期取中位(可改 max 抢量)

# --- 朋友圈版位专属设置(运营标准模板,待提供)---
FEED_AD_SETTING_HEAD_IMAGE_URL = None                 # TODO
FEED_AD_SETTING_NICK_NAME = None                      # TODO
FEED_AD_SETTING_CONVERSION_BUTTON_TEXT = "查看详情"   # 默认

# --- 单账户起步广告条数(Cold Start)---
COLD_START_PER_ACCOUNT_AD_COUNT = 3                   # 对应 3 种 site_set 组合
COLD_START_TOTAL_ADS = COLD_START_PER_ACCOUNT_AD_COUNT * len(WHITELIST_ACCOUNTS)  # 跟随 DB

# ═══════════════════════════════════════════
# 输出路径配置
# ═══════════════════════════════════════════
OUTPUTS_DIR = Path(__file__).parent / "outputs"
RAW_DATA_DIR = OUTPUTS_DIR / "raw"  # 创意级原始 CSV
AD_STATUS_DIR = OUTPUTS_DIR / "ad_status"  # 广告状态 CSV
REPORTS_DIR = OUTPUTS_DIR / "reports"  # 决策报告
EXECUTION_LOG_DIR = OUTPUTS_DIR / "execution_log"  # 执行审计日志
DATA_DIR = OUTPUTS_DIR / "data"  # 运行时数据（如调整历史）
ADJUSTMENT_HISTORY_PATH = DATA_DIR / "adjustment_history.json"

# ═══════════════════════════════════════════
# 人群包系数（保留，用于展示）
# ═══════════════════════════════════════════
AUDIENCE_COEFFICIENTS = {
    "R500":    3.0,
    "R330+":   2.5,
    "R330":    2.0,
    "R180":    1.5,
    "R100":    1.2,
    "R50":     1.0,
    "R10":     1.0,
    "R2":      1.0,
    "default": 1.0,
}

# 从广告名称提取 R 值的匹配顺序
AUDIENCE_TIER_PATTERNS = [
    ("R500",   ["R500", "R_500", "r500"]),
    ("R330+",  ["回流330+", "回流330+-", "回流q330", "330+全品类", "R330+", "R_330+"]),
    ("R330",   ["回流330", "R330", "R_330", "定向330", "r330", "r300"]),
    ("R180",   ["回流180", "R180", "R_180", "定向180", "r180",
                "r180-330", "r180-300", "R100-180", "R_100-180", "r100-180"]),
    ("R100",   ["回流100", "R100", "R_100", "定向100", "r100", "R50-100"]),
    ("R50",    ["回流50", "R50", "R_50", "r50"]),
    ("R10",    ["R_10", "R10", "r10"]),
    ("R2",     ["R_2", "R2", "r2"]),
]


# ═══════════════════════════════════════════════════════════════════
# [MODULE B / 创意搭建] 模块 B 主循环配置
# ═══════════════════════════════════════════════════════════════════
# 模块 B = 给广告挂创意(creative)的子系统,与模块 A(广告新建)对偶。
# 数据流:find_ads_needing_creatives → 关联点过滤 → 召回素材 → POST 创意。

# --- 创意补量目标(单广告期望创意数)---
# 2026-07-09:用户确认最终合格创意目标调整为 8 个
# 生产阶段可继续测试 15-30(腾讯经验下限,MIN_CREATIVES_PER_AD)
# 这是 find_ads_needing_creatives 阈值 + 补量目标的**同一个语义变量**,不要拆
TARGET_CREATIVES_PER_AD = int(os.getenv("TARGET_CREATIVES_PER_AD", "8"))
CREATIVE_PREPARE_MAX_WORKERS = int(os.getenv("CREATIVE_PREPARE_MAX_WORKERS", "1"))
CREATIVE_PREPARE_TASK_BUFFER = int(os.getenv("CREATIVE_PREPARE_TASK_BUFFER", "16"))

# --- 主循环 try-fallback 限额(防无限召回)---
# 单广告最多尝试 N 条 landing,超过即放弃此条创意(不影响广告剩余 to_add)
# 2026-07-01 用户确认:视频获取/尝试上限 100 条
MAX_LANDING_ATTEMPTS_PER_AD = 100
# 单 landing 最多尝试 N 条素材(召回 top N)
MAX_MATERIAL_PER_LANDING = 10
# 内容服务返回的内容品类黑名单。为空则不过滤;多个品类用英文逗号分隔。
LANDING_EXCLUDED_CATEGORIES = {
    v.strip()
    for v in os.getenv("LANDING_EXCLUDED_CATEGORIES", "早中晚好,祝福音乐,历史名人").split(",")
    if v.strip()
}

# --- 承接视频风险审核(2026-06-29 接入)---
# 在 xcx/save 之前调用 piaoquantv 风险标签接口。高风险视频直接跳过,继续尝试下一条 landing。
VIDEO_RISK_CHECK_ENABLED = True
VIDEO_RISK_API_URL = os.getenv(
    "VIDEO_RISK_API_URL",
    "https://longvideoapi.piaoquantv.com/longvideoapi/openapi/video/getVideoTagIds",
)
# 风险等级映射:tag_id -> level,level 越高风险越大。
VIDEO_RISK_TAG_LEVELS = {
    "85856": 1,
    "85862": 2,
    "85863": 3,
    "85864": 4,
    "85865": 5,
    "85866": 6,
    "85867": 7,
    "85868": 8,
    "85869": 9,
    "85870": 10,
}
# 默认允许 0-5,拦截 6-10。可通过环境变量临时调整。
VIDEO_RISK_MAX_ALLOWED_LEVEL = int(os.getenv("VIDEO_RISK_MAX_ALLOWED_LEVEL", "5"))
VIDEO_RISK_API_TIMEOUT_SECONDS = int(os.getenv("VIDEO_RISK_API_TIMEOUT_SECONDS", "10"))

# --- 素材召回质量过滤(2026-06-10 用户确认,batchByText 升级)---
# 服务端 ranking 参数只保留 simThreshold(语义相关),其余加权全部置 0。
# 客户端只用 score >= simThreshold 做硬筛,再按历史消耗 cost 倒序排序。
# 2026-08-07 用户从 0.8 调整为 0.7(释放相似度墙,补充素材供给)。
RECALL_SIM_THRESHOLD = 0.7
RECALL_ALPHA = 0
RECALL_W_CTR = 0
RECALL_W_CVR = 0
RECALL_W_ROI = 0
RECALL_W_OPEN_RATE = 0
RECALL_W_FISSION_RATE = 0
RECALL_DECONSTRUCT_BOOST = 0
RECALL_DAYS = 180                                 # 投放统计天数(2026-06-10 修正:30→180,跟 admin 后台一致,冷门素材需长周期)
RECALL_DISPLAY_K = 30                             # 服务端展示条数
RECALL_PARALLEL_MAX_WORKERS = int(os.getenv("RECALL_PARALLEL_MAX_WORKERS", "4"))
RECALL_QUERY_LIMIT_PER_VIDEO = int(os.getenv("RECALL_QUERY_LIMIT_PER_VIDEO", "12"))
RECALL_MIN_IMPRESSIONS = 0                        # 保留兼容配置;当前不作为硬筛。
RECALL_MIN_CTR = 0.0                              # 保留兼容配置;当前不作为硬筛。
# --- 素材召回历史成本门槛(2026-08-06 用户确认;2026-08-07 调整:外部素材不走此门槛)---
# 仅历史投放素材(内部)适用:历史成本(cost, 元) >= 此值才进入候选池。
# 严格语义:无历史成本数据(cost is None,从未投放过)的素材同样排除。
# 注:设为 0 只关闭金额门槛,cost is None 仍排除(严格语义)。
# 外部合作素材召回(recall_external_materials_for_video)传 apply_cost_filter=False
# 绕过此门槛,以 EXTERNAL_RECALL_MIN_UV 的访问 UV 门槛把关。
RECALL_MIN_COST_YUAN = int(os.getenv("RECALL_MIN_COST_YUAN", "100"))
RECALL_SOURCE_LABELS = ["内部素材"]               # 只要内部
MAX_SAME_LANDING_PER_AD_IN_RUN = int(os.getenv("MAX_SAME_LANDING_PER_AD_IN_RUN", "1"))
RECALL_CONFIG_CODES_FULL = [
    "VIDEO_TOPIC", "VIDEO_INSPIRATION", "VIDEO_PURPOSE",
    "VIDEO_KEYPOINT", "VIDEO_TITLE",
    "RESULT_LOG_TOPIC", "RESULT_LOG_THEME",
    "RESULT_LOG_KEYWORDS", "RESULT_LOG_NARRATION",
    "INSPIRATION_SUBSTANCE", "KEYPOINT_SUBSTANCE", "PURPOSE_SUBSTANCE",
    "INSPIRATION_FORM", "KEYPOINT_FORM", "PURPOSE_FORM",
]

# --- 创意文案池---
# 2026-07-01 用户确认:创意文案统一使用"打开看看"。
CREATIVE_DESCRIPTION_POOL = [
    "打开看看",
]
CREATIVE_DESCRIPTION_COUNT_PER_AD = 1

# --- 审批开关 ---
# True(默认):Phase 1 准备后写待审批 CSV + 发飞书 sheet → 等运营审批 → Phase 3 POST
# False:Phase 1 跑完直接 Phase 3 POST(skip 飞书)— 用于自动化 cron + 信任规则的场景
# 环境变量 CREATION_APPROVAL_REQUIRED 优先（"1"/"true"/"yes"=需要审批，"0"/"false"/"no"=跳过）
CREATION_APPROVAL_REQUIRED = os.getenv(
    "CREATION_APPROVAL_REQUIRED", "true"
).strip().lower() not in {"0", "false", "no"}

# 审批超时(分钟,与现有调控审批配置对齐)
CREATION_APPROVAL_TIMEOUT_MINUTES = 120

# 飞书 chat_id:广告审批、创意审批和执行汇报统一使用 FEISHU_CREATION_CHAT_ID。

# ═══════════════════════════════════════════
# 阿里云 SLS 日志上报(2026-06-11 接入,K8s pod 中 SDK 直发)
# ═══════════════════════════════════════════
# 从 os.environ 读取(.env 已 load_dotenv),任一缺失 → SLS_ENABLED=False → 不上报,只本地 file
SLS_ENDPOINT          = os.environ.get("SLS_ENDPOINT", "")          # 例 cn-hangzhou.log.aliyuncs.com
SLS_ACCESS_KEY_ID     = os.environ.get("SLS_ACCESS_KEY_ID", "")     # RAM 子账号 AK(只授 Log:PutLogs 权限)
SLS_ACCESS_KEY_SECRET = os.environ.get("SLS_ACCESS_KEY_SECRET", "") # 同上的 SK
SLS_PROJECT           = os.environ.get("SLS_PROJECT", "auto-put-tecent")
SLS_INFO_LOGSTORE     = os.environ.get("SLS_INFO_LOGSTORE", os.environ.get("SLS_LOGSTORE", "info-log"))
SLS_ERROR_LOGSTORE    = os.environ.get("SLS_ERROR_LOGSTORE", "error-log")
SLS_LOGSTORE          = SLS_INFO_LOGSTORE  # 兼容旧变量名,默认对应 info-log

# 全开:任一缺失则降级为 False,主链路不受影响
SLS_ENABLED = bool(
    SLS_ENDPOINT and SLS_ACCESS_KEY_ID and SLS_ACCESS_KEY_SECRET
    and SLS_PROJECT and SLS_INFO_LOGSTORE and SLS_ERROR_LOGSTORE
)

# 上报等级 — 用户决策(2026-06-11):所有 INFO+ 上报
# 注:material_recall 每个 landing 打 4 条 INFO,日 cron 量级约 5k-20k 条,SLS 流量成本几 RMB/月
SLS_LOG_LEVEL = os.environ.get("SLS_LOG_LEVEL", "INFO").upper()

# QueuedLogHandler 内部异步队列参数(SDK 默认 + 微调,避免长连接 idle 断)
SLS_BATCH_SIZE_MAX = int(os.environ.get("SLS_BATCH_SIZE_MAX", "1024"))
SLS_PUT_WAIT_MS = int(os.environ.get("SLS_PUT_WAIT_MS", "2000"))
