"""首页轮播 feed:全平台真实比价记录 + 种子兜底「混播」,以及种子表 CRUD。

feed 取最近的成功且省到钱的比价记录(脱敏用户名);不足 N 条时用 ops_marquee_seed 的种子补齐,
保证轮播永不空。真实够时种子不出现。

种子是「生成规则」:用户名可空(空→随机合成脱敏名,避开同屏撞名)、金额是区间(每次随机取值)、
feed 公平随机抽取(不看 sort_order),所有启用种子都有机会露出。

脱敏名规则(对齐 PRD「用户标识打码规则」,按字符数算、中英文皆适用):
  - 有昵称 → 昵称脱敏:n≥5「首+***+末」(AAA省钱小能手→A***手 / Micky_ 毛→M***毛),
    n=4「首+**+末」(你好不好→你**好),n≤3「首+**」(小确呀→小** / 阿强→阿** / 喵→喵**);
  - 没昵称(真实用户)→「用户」+5星+id 后 2 位(用户*****08),按 user_id 稳定。
真实条按上述 PRD 规则脱敏。种子 / 兜底无真实用户(无昵称/无 id,套不上「用户*****+id后2位」),
仍本地合成多样假名再走同一套昵称脱敏(避开同屏撞名),让横条不至清一色;种子留空 / 旧「用户****xxx」
模板名一律重新合成(自愈历史种子,无需迁移)。
假名合成:纯本地语料**组合生成**(中文姓池×名字池 拼真名 / 中文网络昵称 / 英文昵称 / 英文名带数字尾),
组合空间上万、中文为主英文为辅,脱敏后像真实异质用户群。**真实条按 `user_id` 播种确定性合成**(同一用户
每次展示恒定、不同用户各异),刷新/翻页不变脸;种子 / 兜底用运行时随机源出多样填充。不依赖外部库。
"""
from __future__ import annotations

import random
from datetime import datetime, timedelta

from sqlalchemy import delete, func, select, update
from sqlalchemy.orm import Session

from app.core.rewards import CN_TZ
from app.models.comparison import ComparisonRecord
from app.models.ops_marquee_seed import OpsMarqueeSeed
from app.models.user import User

# feed 运行时随机源(每次请求结果不同 = 轮播想要的「鲜活感」)
_rng = random.Random()

# 真实条金额上限(300 元):超过视为异常 / bug 值,不进轮播(防「节省 999 元」穿帮)
_REAL_MAX_CENTS = 30000
# 种子金额防呆上限(1000 元):运营手滑填超大值时拦下
_SEED_MAX_CENTS = 100000

# 真实记录查询缓存:首页人人都看、真实数据变化慢 → 缓存原始行 ~30s,省掉高频 DB 往返。
# 展示层随机(抽样/金额/时间/名字合成)仍每次重算,缓存只省查询;新记录最多晚 30s 进轮播,可接受。
_REAL_ROWS_TTL_SECONDS = 30
_REAL_ROWS_FETCH_CAP = 600  # 一次多取些,够 limit≤30 去重后取数;命中缓存后复用
_real_rows_cache: dict = {"at": None, "rows": None}

# ===== 用户标识脱敏(对齐 PRD) + 种子无真实昵称时的假名合成 =====
# 脱敏规则(按字符数,中英文皆适用):有昵称→n≥5「首+***+末」、n=4「首+**+末」、n≤3「首+**」;
# 没昵称(真实用户)→「用户」+5星+id 后 2 位(用户*****08)。
# 种子/兜底没有真实用户(无昵称/无 id),本地用姓/名字池组合真名 + 网络昵称语料(中/英)合成假名,
# 再走同一套昵称脱敏,让横条像真实异质用户群、不清一色(PRD 只约束真实用户,种子是运营填充)。
_ANON_STARS = "*" * 5  # 「用户」匿名串固定 5 星(用户*****xx,对齐 PRD)

# 中文真名组合池:姓(百家姓常见 ~100)× 名字单字(~120)拼「姓 + 1~2 字」,组合空间上万。
# 脱敏后只露「姓*」或「姓*末」,故无需真实姓名库——组合够多即看着各异(替代原 Faker zh_CN)。
_SURNAME_ZH = list(
    "王李张刘陈杨赵黄周吴徐孙胡朱高林何郭马罗梁宋郑谢韩唐冯于董萧程曹袁邓许傅沈曾彭吕苏卢蒋蔡贾丁"
    "魏薛叶阎余潘杜戴夏钟汪田任姜范方石姚谭廖邹熊金陆郝孔白崔康毛邱秦江史顾侯邵孟龙万段钱汤尹黎"
    "易常武乔贺赖龚文施洪丰房邢"
)
_GIVEN_ZH = list(
    "伟芳娜秀英敏静丽强磊军洋勇艳杰娟涛明超霞平刚桂兰华健世广义兴良海山仁波宁贵福生龙元全国胜学祥"
    "才发新利清飞彬富顺信子昌成康星光天达安岩中茂进有坚和彪博诚先敬震振壮会思群豪心邦承乐绍功松善"
    "厚庆民友裕河哲亮政谦亨奇之轮翰朗伯宏言若鸣朋斌栋维启克伦翔旭鹏泽晨辰士以建家致树炎德行时泰盛雄"
)
# 英文名池:与英文昵称互补,偶尔带数字尾(jay87)更像注册用户名(替代原 Faker en_US)。
_FIRST_EN = [
    "Alex", "Chris", "Sam", "Jamie", "Taylor", "Jordan", "Casey", "Morgan", "Riley", "Jessie",
    "Robin", "Drew", "Lee", "Max", "Kim", "Ray", "Dana", "Pat", "Terry", "Jay",
    "Kelly", "Sky", "Ash", "Nico", "Remy", "Quinn", "Reese", "Sage", "Toni", "Val",
]

# 网络昵称语料:补一份「省钱小能手 / SaveKing」式网络昵称更像真实 app 用户。想更丰富往里加即可。
_NICK_ZH = [
    "省钱小能手", "薅羊毛专业户", "吃货一枚", "干饭人", "奶茶续命", "精打细算过日子",
    "会过日子的人", "捡漏王", "外卖救星", "养生少年", "持家有道", "比价狂魔",
    "月光族逆袭", "摸鱼达人", "暴富锦鲤", "美食猎人", "折扣猎手", "隐形贫困人口",
    "打工不打烊", "佛系青年", "元气满满", "一只小馋猫", "爱吃的小朋友", "省钱才是硬道理",
]
_NICK_EN = [
    "SaveKing", "DealHunter", "FoodieLife", "BudgetBoss", "CouponQueen", "ThriftyOne",
    "SnackLover", "BargainHero", "PennyWise", "SmartShopper", "DiscountNinja", "HungryPanda",
    "MoneyMaster", "CheapEats", "SaverPro", "FrugalFox", "NoodleKing", "BubbleTeaFan",
    "GoldenSaver", "ValueHunter", "SnackAttack", "LazyFoodie",
]


def _mask_nickname(nick: str) -> str:
    """昵称脱敏(按字符数,中英文皆可,对齐 PRD「用户标识打码规则」):
    n≥5 → 首+***+末(AAA省钱小能手→A***手 / Micky_ 毛→M***毛);
    n=4 → 首+**+末(你好不好→你**好 / Lady→L**y);
    n≤3 → 首+**(小确呀→小** / 阿强→阿** / 喵→喵** / Lay→L**)。
    调用方需保证 nick 已 strip 且非空。"""
    n = len(nick)
    if n >= 5:
        return nick[0] + "***" + nick[-1]
    if n == 4:
        return nick[0] + "**" + nick[-1]
    return nick[0] + "**"


def _mask_anon(user_id: int) -> str:
    """没昵称(真实用户,对齐 PRD):「用户」+ 5 星 + id 后 2 位(不足 2 位前补 0):用户*****08。
    注:user_id 为整数,「id 后 2 位」取末两位数字(PRD 示例「用户*****x8」的 x 为占位符)。"""
    return "用户" + _ANON_STARS + f"{user_id % 100:02d}"


def _synth_full_name(rng: random.Random) -> str:
    """合成一个完整(未脱敏)假名:中文真名(姓+1~2字) / 中文网络昵称 / 英文昵称 / 英文名(可带数字尾) 混播,
    中文为主(~70%)英文为辅(~30%)。**全程用传入 rng** → 同种子复现、不同种子各异、组合空间上万。"""
    r = rng.random()
    if r < 0.35:
        # 中文真名:姓 + 1~2 个名字字(60% 概率两字),脱敏后露「姓*」或「姓*末」
        given = rng.choice(_GIVEN_ZH) + (rng.choice(_GIVEN_ZH) if rng.random() < 0.6 else "")
        return rng.choice(_SURNAME_ZH) + given   # 王伟 / 李秀兰
    if r < 0.70:
        return rng.choice(_NICK_ZH)              # 中文网络昵称:省钱小能手
    if r < 0.85:
        return rng.choice(_NICK_EN)              # 英文昵称:SaveKing
    base = rng.choice(_FIRST_EN)                 # 英文名,半数带数字尾:Jay87 / Alex
    return base + (str(rng.randint(1, 99)) if rng.random() < 0.5 else "")


def _synth_masked_name(rng: random.Random) -> str:
    """合成一条「已脱敏」假名(供种子 / 兜底用,真实用户已改走 PRD 规则)。用传入 rng 决定一切随机。"""
    full = _synth_full_name(rng).strip()
    return _mask_nickname(full) if full else "用户" + _ANON_STARS + f"{rng.randint(0, 99):02d}"


def _mask_real(nickname: str | None, user_id: int) -> str:
    """真实用户脱敏(对齐 PRD「用户标识打码规则」):设过昵称→昵称脱敏(中英文皆可);
    没昵称→「用户」+5星+id 后 2 位(用户*****08),按 user_id 稳定、刷新不变脸。"""
    nick = (nickname or "").strip()
    if nick:
        return _mask_nickname(nick)
    return _mask_anon(user_id)


def _synth_name(rng: random.Random) -> str:
    """合成一条脱敏名(种子留空 / 旧模板名 / 兜底用):绝大多数=假名脱敏,少数=用户+随机 2 位(用户*****08)。"""
    if rng.random() < 0.15:
        return "用户" + _ANON_STARS + f"{rng.randint(0, 99):02d}"
    return _synth_masked_name(rng)


def _unique_name(used: set[str]) -> str:
    """随机合成一个不与 used 撞的脱敏名(种子留空 / 旧模板名 / 兜底用),防同屏撞名。"""
    for _ in range(60):
        n = _synth_name(_rng)
        if n not in used:
            return n
    return "用户" + _ANON_STARS + f"{_rng.randint(0, 99):02d}"  # 兜底:数字尾号天然好去重


def _validate_amount(min_cents: int, max_cents: int) -> None:
    if min_cents < 0 or max_cents < 0:
        raise ValueError("金额需为非负整数(分)")
    if max_cents < min_cents:
        raise ValueError("金额上限不得小于下限")
    if max_cents > _SEED_MAX_CENTS:
        raise ValueError(f"金额上限过大(≤ {_SEED_MAX_CENTS // 100} 元)")


def _skewed_amount(lo: int, hi: int) -> int:
    """长尾金额:多数贴近下限、偶尔接近上限(比均匀随机更像真实省钱分布)。
    用 u**2.2 把 [0,1) 均匀值压向 0 → 小额居多;偶尔 u 接近 1 给出大额。
    """
    if hi <= lo:
        return max(0, lo)
    u = _rng.random() ** 2.2
    return lo + int(round((hi - lo) * u))


# 兜底金额区间(真实 + 种子都凑不满 limit 时,内置合成条用):3~35 元
_FALLBACK_MIN_CENTS = 300
_FALLBACK_MAX_CENTS = 3500


# ===== 用户侧:轮播 feed(真实 + 种子混播) =====
def _recent_real_rows(db: Session) -> list[tuple[int, int, str | None]]:
    """近期 success 且省到钱的 (user_id, saved_cents, nickname),按 created_at desc;带 ~30s 进程内缓存。
    join user 取真实昵称用于脱敏(无昵称则展示层走「用户+id 后 3 位」)。
    返回纯元组(脱离 session),可安全跨请求复用。极端并发下偶尔多查一次(无锁、幂等),纯门面无副作用。
    """
    now = datetime.now(CN_TZ)
    cached, at = _real_rows_cache["rows"], _real_rows_cache["at"]
    if cached is not None and at is not None and (now - at).total_seconds() < _REAL_ROWS_TTL_SECONDS:
        return cached
    rows = db.execute(
        select(ComparisonRecord.user_id, ComparisonRecord.saved_amount_cents, User.nickname)
        .join(User, User.id == ComparisonRecord.user_id)
        .where(
            ComparisonRecord.status == "success",
            ComparisonRecord.saved_amount_cents > 0,
            ComparisonRecord.saved_amount_cents <= _REAL_MAX_CENTS,
        )
        .order_by(ComparisonRecord.created_at.desc())
        .limit(_REAL_ROWS_FETCH_CAP)
    ).all()
    out = [(int(uid), int(sc), nick) for uid, sc, nick in rows]
    _real_rows_cache["rows"], _real_rows_cache["at"] = out, now
    return out


def get_feed(db: Session, limit: int = 8) -> list[dict]:
    """返回最多 limit 条 {masked_user, saved_amount_cents, time(HH:MM:SS 北京)}。

    真实条:success 且 0 < saved ≤ 上限,按 user 去重(同一用户只取最新一条,避免单人刷屏)。
    不足用启用的种子补齐——**公平随机抽取** need 个(而非固定取前 N),让所有种子都有机会露出;
    种子用户名留空则随机合成(避开撞名),金额取**长尾随机**(小额居多、偶尔大额,更像真实分布)。
    真实 + 种子仍不满 limit → 内置合成条**补满**,保证轮播既不空也不稀疏。
    展示时间统一「刷新」成相对现在的最近时刻(从 now 往前**随机抖动**递减),保证轮播永远像刚发生、
    节奏自然不机械(真实用户/金额不变,只换展示时间——避免旧测试数据 / 低谷期记录显示成过时时间)。
    """
    # 真实条:取较多近期记录(带 ~30s 缓存)后按 user 去重;金额超上限的异常值已在查询剔除。
    rows = _recent_real_rows(db)

    items: list[dict] = []
    used_names: set[str] = set()
    seen_users: set[int] = set()
    for uid, sc, nick in rows:
        if uid in seen_users:
            continue
        seen_users.add(uid)
        name = _mask_real(nick, uid)
        used_names.add(name)  # 真实名按昵称/id 稳定;偶发撞名可接受
        items.append({"masked_user": name, "saved_amount_cents": int(sc)})
        if len(items) >= limit:
            break

    need = limit - len(items)
    if need > 0:
        seeds = db.execute(
            select(OpsMarqueeSeed).where(OpsMarqueeSeed.enabled.is_(True))
        ).scalars().all()
        # 公平随机抽取 need 个(池子够大则不放回抽样;否则全用并打散),让所有启用种子都有机会露出。
        chosen = _rng.sample(seeds, need) if len(seeds) > need else list(seeds)
        _rng.shuffle(chosen)
        for s in chosen:
            # 用户名:旧的「用户****xxx」统一模板名 / 留空 → 一律走新混合合成(避开同屏撞名、自愈历史种子);
            # 仅运营手动设的非模板真名才原样用。
            fixed = (s.masked_user or "").strip()
            name = fixed if fixed and not fixed.startswith("用户****") else _unique_name(used_names)
            used_names.add(name)
            # 金额:在 [min,max] 取长尾随机值(小额居多、偶尔大额;固定金额则 min==max)。
            lo = max(0, int(s.min_cents))
            hi = max(lo, int(s.max_cents))
            items.append({"masked_user": name, "saved_amount_cents": _skewed_amount(lo, hi)})

    # 兜底:真实 + 种子仍凑不满 limit(种子被全停用 / 数量太少)→ 用内置合成补满,
    # 保证轮播既不空也不稀疏(稀疏的几条循环同样像假)。
    while len(items) < limit:
        name = _unique_name(used_names)
        used_names.add(name)
        items.append({
            "masked_user": name,
            "saved_amount_cents": _skewed_amount(_FALLBACK_MIN_CENTS, _FALLBACK_MAX_CENTS),
        })

    # 统一赋「最近」时间:从 now 往前**随机抖动**递减,避免固定节奏被看出规律。
    # 首条几十秒前;其余多数 1~5 分钟,偶尔扎堆(20~55s)或较长(5~9 分钟),降序、像真实流水。
    cursor = datetime.now(CN_TZ)
    for i, it in enumerate(items):
        if i == 0:
            gap = _rng.randint(8, 40)
        else:
            r = _rng.random()
            if r < 0.2:
                gap = _rng.randint(20, 55)       # 偶尔扎堆
            elif r < 0.85:
                gap = _rng.randint(60, 300)      # 常规 1~5 分钟
            else:
                gap = _rng.randint(300, 540)     # 偶尔较长 5~9 分钟
        cursor = cursor - timedelta(seconds=gap)
        it["time"] = cursor.strftime("%H:%M:%S")
    return items


# ===== 运营侧:种子 CRUD =====
def list_seeds(db: Session) -> list[OpsMarqueeSeed]:
    return list(
        db.execute(
            select(OpsMarqueeSeed).order_by(OpsMarqueeSeed.sort_order, OpsMarqueeSeed.id)
        ).scalars().all()
    )


def create_seed(
    db: Session,
    *,
    masked_user: str | None = None,
    min_cents: int,
    max_cents: int,
    enabled: bool = True,
    sort_order: int = 0,
    commit: bool = True,
) -> OpsMarqueeSeed:
    _validate_amount(min_cents, max_cents)
    seed = OpsMarqueeSeed(
        masked_user=(masked_user.strip() or None) if masked_user else None,
        min_cents=min_cents,
        max_cents=max_cents,
        enabled=enabled,
        sort_order=sort_order,
    )
    db.add(seed)
    if commit:
        db.commit()
        db.refresh(seed)
    else:
        db.flush()
    return seed


def update_seed(
    db: Session,
    seed_id: int,
    *,
    masked_user: str | None = None,
    min_cents: int | None = None,
    max_cents: int | None = None,
    enabled: bool | None = None,
    sort_order: int | None = None,
    commit: bool = True,
) -> OpsMarqueeSeed:
    """改种子。masked_user 约定:不传(None)=不改;传空串=清空(改回随机合成);传值=固定该名。"""
    seed = db.get(OpsMarqueeSeed, seed_id)
    if seed is None:
        raise ValueError("种子不存在")
    if masked_user is not None:
        seed.masked_user = masked_user.strip() or None
    if min_cents is not None or max_cents is not None:
        new_min = min_cents if min_cents is not None else seed.min_cents
        new_max = max_cents if max_cents is not None else seed.max_cents
        _validate_amount(new_min, new_max)
        seed.min_cents = new_min
        seed.max_cents = new_max
    if enabled is not None:
        seed.enabled = enabled
    if sort_order is not None:
        seed.sort_order = sort_order
    if commit:
        db.commit()
        db.refresh(seed)
    else:
        db.flush()
    return seed


def bulk_generate(
    db: Session,
    *,
    count: int,
    min_cents: int,
    max_cents: int,
    enabled: bool = True,
    commit: bool = True,
) -> list[OpsMarqueeSeed]:
    """批量生成 count 条「自动合成名」种子(masked_user=NULL),金额区间 [min,max]。
    冷启动铺量用;sort_order 续在当前最大值之后。
    """
    if not (1 <= count <= 200):
        raise ValueError("数量需在 1~200 之间")
    _validate_amount(min_cents, max_cents)
    base = db.execute(
        select(func.coalesce(func.max(OpsMarqueeSeed.sort_order), 0))
    ).scalar_one()
    created: list[OpsMarqueeSeed] = []
    for i in range(1, count + 1):
        seed = OpsMarqueeSeed(
            masked_user=None,
            min_cents=min_cents,
            max_cents=max_cents,
            enabled=enabled,
            sort_order=base + i,
        )
        db.add(seed)
        created.append(seed)
    if commit:
        db.commit()
        for s in created:
            db.refresh(s)
    else:
        db.flush()
    return created


def delete_seed(db: Session, seed_id: int, *, commit: bool = True) -> bool:
    seed = db.get(OpsMarqueeSeed, seed_id)
    if seed is None:
        return False
    db.delete(seed)
    if commit:
        db.commit()
    else:
        db.flush()
    return True


def batch_delete(db: Session, ids: list[int], *, commit: bool = True) -> int:
    """批量删除选中的种子,返回实际删除条数(不存在的 id 自动跳过)。"""
    if not ids:
        return 0
    n = db.execute(delete(OpsMarqueeSeed).where(OpsMarqueeSeed.id.in_(ids))).rowcount
    if commit:
        db.commit()
    else:
        db.flush()
    return int(n or 0)


def batch_set_enabled(db: Session, ids: list[int], enabled: bool, *, commit: bool = True) -> int:
    """批量启用 / 停用选中的种子,返回实际更新条数。"""
    if not ids:
        return 0
    n = db.execute(
        update(OpsMarqueeSeed).where(OpsMarqueeSeed.id.in_(ids)).values(enabled=enabled)
    ).rowcount
    if commit:
        db.commit()
    else:
        db.flush()
    return int(n or 0)
