"""
text_variants.py — 文案双轨主入口（≤100行）
轨A: generate_text_variants — 调 LLM 出 N 角度 JSON
轨B: text_import_handler   — 导入外部文案进候选池

prompt 组装/解析见 _text_prompt.py；评分/去重见 text_scoring.py
"""
from __future__ import annotations
import asyncio
import logging
import os
from typing import Any

from .constants import MAX_OPTIMIZE_ROUNDS
from ._text_prompt import COPY_SYSTEM, build_prompt, parse_json_array, build_local_drafts
from .text_scoring import score_copy, dedupe_copies
from .llm_scorer import llm_score_copy
from .banned_word_checker import check_and_fix, build_entries_from_db, CheckResult

logger = logging.getLogger(__name__)


async def _call_llm(client: Any, prompt: str, max_tokens: int = 8192) -> str:
    """统一 LLM 调用，client 由 worker 注入，隔离 key。
    G1坑修复：AIClients 没有 .chat.completions，正确方法是 .chat_complete()
    S8: 503/429 指数退避重试（最多3次，2^attempt 秒），其他异常直接降级返 ''。
    max_tokens 由调用方按批量缩放：opus 会尽量填满输出空间，8192 token 的生成
    单批 >60s 必撞 apiports 网关上限返 503(task46 实测每请求恰挂 ~61s)。实测单条
    max_tokens=1500~2500 仅 16~18s。故按条数动态收，墙钟压进 60s 网关窗口内。
    """
    import httpx

    # 倩倩姐2026-06-13拍板"加大重试+拉长退避"：apiports负载波动时单条opus也会被
    # 拖过60s返503，短退避(1/2/4s)赶不开高负载窗口。故重试5次、退避拉长到最长30s，
    # 给中转站负载回落留时间。墙钟换稳定(MVP免费阶段可接受)。
    max_attempts = 5
    backoff = [5, 10, 20, 30]  # 第1~4次重试前等待秒数，拉长跨过apiports高负载窗口
    for attempt in range(max_attempts):
        try:
            return await client.chat_complete(
                messages=[
                    {"role": "system", "content": COPY_SYSTEM},
                    {"role": "user",   "content": prompt},
                ],
                model=client._model,
                max_tokens=max_tokens,
                temperature=0.75,
            )
        except httpx.HTTPStatusError as exc:
            status = exc.response.status_code if exc.response is not None else 0
            if status in (503, 429) and attempt < max_attempts - 1:
                wait = backoff[min(attempt, len(backoff) - 1)]
                logger.warning(
                    "LLM 返回 %s，第%d/%d次重试，等待 %ds: %s",
                    status, attempt + 1, max_attempts - 1, wait, exc,
                )
                await asyncio.sleep(wait)
                continue
            logger.error("LLM HTTP错误(不可重试或已达上限): %s: %s", type(exc).__name__, exc)
            return ""
        except Exception as exc:
            # 其他异常（超时/网络断开等）不重试，直接降级
            logger.error("LLM 调用失败: %s: %s", type(exc).__name__, exc)
            return ""
    return ""


# apiports 网关单次响应有 ~60s 上限，claude 一次生成 >4 条长文案会超时返 503。
# 故分批：每批最多 4 条，串行调用合并。批大小可经 TEXT_BATCH_SIZE 调。
TEXT_BATCH_SIZE = int(os.environ.get("TEXT_BATCH_SIZE", "4"))


async def _generate_one_batch(llm_client: Any, product: dict, batch_n: int, extra: str) -> list[dict]:
    """生成一批 batch_n 条，含解析重试（最多2次）。失败返回空列表。
    max_tokens 按条数缩放(每条约 1800 token，封顶 8192)，压进 apiports 60s 网关窗口。"""
    batch_max_tokens = min(8192, max(1800, batch_n * 1800))
    for attempt in range(2):
        raw = await _call_llm(llm_client, build_prompt(product, batch_n, extra_rules=extra), batch_max_tokens)
        parsed = parse_json_array(raw)
        if parsed:
            return parsed
        logger.warning("文案批(%d条)第%d次解析失败%s", batch_n, attempt + 1,
                       "，重试" if attempt == 0 else "，放弃本批")
    return []


async def _generate_in_batches(llm_client: Any, product: dict, count: int, extra: str) -> list[dict]:
    """把 count 条按 TEXT_BATCH_SIZE 分批，串行调用合并。
    串行而非并发：opus 单批就慢(~300s)且 apiports 限并发，多批 gather 会触发
    大面积 503 雪崩(task45 实测)。故改串行，墙钟换稳定。"""
    sizes: list[int] = []
    remaining = count
    while remaining > 0:
        n = min(TEXT_BATCH_SIZE, remaining)
        sizes.append(n)
        remaining -= n
    collected: list[dict] = []
    for n in sizes:
        r = await _generate_one_batch(llm_client, product, n, extra)
        collected.extend(r)
    return collected


async def generate_text_variants(
    llm_client: Any,
    product: dict,
    count: int,
    previous_copies: list[dict] | None = None,
    banned_word_rows: list[dict] | None = None,
    flywheel_context: str = "",
) -> list[dict]:
    """轨A：一次出 count 条不同角度文案，三层兜底，自动优化循环"""
    banned_entries = build_entries_from_db(banned_word_rows or [])
    extra = flywheel_context

    copies: list[dict] = await _generate_in_batches(llm_client, product, count, extra)
    if not copies:
        copies = list(build_local_drafts(product, count))  # generator → list

    candidates: list[dict] = []
    for c in copies:
        ban: CheckResult = check_and_fix(
            f"{c.get('title','')} {c.get('content','')}",
            banned_entries or None,
        )
        scored = await llm_score_copy(llm_client, c, product, [e.word for e in banned_entries])
        c.update({"source": "ai", "score": scored["score"], "score_detail": scored["score_detail"],
                  "passed": scored["passed"], "banned_word_status": ban.status,
                  "verdict": scored.get("verdict", ""), "summary": scored.get("summary", "")})
        if ban.status == "auto_fixed" and ban.fixed_text:
            c["content"] = ban.fixed_text
        candidates.append(c)

    failed = [c for c in candidates if not c["passed"] and c["banned_word_status"] != "hard_block"]
    # 优化轮默认关闭：apiports 60s 网关限制下优化轮的 _call_llm 常需白等 60s 才 503，
    # 严重拖慢出文案（实测 +100s+）。质量优化等北哥 prompt 方案到位再开（架构已留位）。
    optimize_enabled = os.environ.get("TEXT_OPTIMIZE_ENABLED", "false").lower() == "true"
    rounds = MAX_OPTIMIZE_ROUNDS if optimize_enabled else 0
    for _ in range(rounds):
        if not failed:
            break
        # 优化轮也受 60s 网关上限约束：一次最多重生成 TEXT_BATCH_SIZE 条
        batch_failed = failed[:TEXT_BATCH_SIZE]
        hint = "\n".join(
            f"标题「{c['title']}」{c['score']}分，需改进：" +
            "；".join(d["note"] for d in c.get("score_detail", []) if d["score"] < d["max"] * 0.72)
            for c in batch_failed
        )
        raw2 = await _call_llm(llm_client, build_prompt(
            product, len(batch_failed),
            extra_rules=f"以下文案未达标，请重新生成并改进：\n{hint}\n不要重复已有标题和角度。",
        ), min(8192, max(1800, len(batch_failed) * 1800)))
        if not raw2:
            # LLM 失败（如 503/超时）：优化是锦上添花，原始候选已够用，不再耗时重试
            logger.warning("文案优化轮 LLM 失败，沿用原始候选不再重试")
            break
        for nc in parse_json_array(raw2):
            sc2 = await llm_score_copy(llm_client, nc, product, [e.word for e in banned_entries])
            nc.update({"source": "ai", "score": sc2["score"], "score_detail": sc2["score_detail"],
                       "passed": sc2["passed"], "banned_word_status": "pass",
                       "verdict": sc2.get("verdict", ""), "summary": sc2.get("summary", "")})
            candidates.append(nc)
        failed = [c for c in candidates if not c["passed"]]

    return dedupe_copies(candidates, previous_copies or [])[:count]


def text_import_handler(
    raw_text: str,
    product: dict,
    banned_word_rows: list[dict] | None = None,
) -> dict:
    """轨B：导入外部文案（豆包等）直接进候选池，source=import"""
    banned_entries = build_entries_from_db(banned_word_rows or [])
    lines   = raw_text.strip().splitlines()
    title   = lines[0].strip() if lines else ""
    content = "\n".join(lines[1:]).strip() if len(lines) > 1 else raw_text.strip()
    candidate: dict = {"title": title, "content": content, "tags": [], "angle": "import",
                       "buyingPoint": "", "coverTitle": title, "imageBrief": "", "source": "import"}
    ban    = check_and_fix(f"{title} {content}", banned_entries or None)
    # 轨B(导入外部文案)走机械 score_copy 而非 AI 评委：导入的是用户自带成品，评分仅作
    # 参考展示不卡发布；且本函数同步、改 await 会扩大到调用方。AI 评委只用于轨A生成链路。
    scored = score_copy(candidate, product, [e.word for e in banned_entries])
    candidate.update({"score": scored["score"], "score_detail": scored["score_detail"],
                      "passed": scored["passed"], "banned_word_status": ban.status})
    return candidate
