"""消息通知中心 数据仓库(落库版,查/写 `notification` 表)。

沿用原 notification_mock 的同名函数(list_notifications / unread_count / mark_read /
insert_sample),由内存 mock 迁到落库,**API 契约不变**。

- 读:按 user 隔离、sent_at 倒序;未读数 / 标记已读同口径。
- 写:`create_notification` 是落库统一入口。**业务事件请走 services/notification_events**
  (站内消息 + 厂商 push 一起下发,已接入提现回执/反馈审核/爆料通过/好友下单);
  `build_sample_card` / `insert_sample` 按类型造样例内容,供
  `/api/v1/push/test` 的 createNotification 做「push → 站内已读联动」联调。

排序规则:全列表按 sent_at 倒序(最新在前;同秒再按 id 倒序稳定化),不分组。
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone

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

from app.core import notification_catalog as catalog
from app.models.notification import Notification

# 北京时间:sent_at 统一带 +08:00 下发,前端直接按本地时区渲染「今天/昨天/M月D日」。
_CST = timezone(timedelta(hours=8))


def cash_yuan(cents: int | None) -> str | None:
    """分 → 保留两位小数的元字符串(PRD §3:现金/提现金额保留两位小数)。"""
    if cents is None:
        return None
    return f"{cents // 100}.{cents % 100:02d}"


def as_cst(dt: datetime) -> datetime:
    """把库里取出的时间归一到北京时间(+08:00)再下发,保证接口 sentAt 恒带 +08:00。

    SQLite 的 DateTime 不存时区,取出为 naive(存的就是写入时的 CST 墙上时间)→ 直接贴 +08:00;
    PostgreSQL 的 timestamptz 取出为 aware(通常 UTC)→ 转到 +08:00。两端下发口径一致。
    """
    if dt.tzinfo is None:
        return dt.replace(tzinfo=_CST)
    return dt.astimezone(_CST)


def _fmt_time(dt: datetime) -> str:
    """信息行里「到账时间」等 value 的展示格式。"""
    return dt.strftime("%Y-%m-%d %H:%M")


# ---------------------------------------------------------------------------
# 读:列表 / 未读数 / 标记已读
# ---------------------------------------------------------------------------


def _unread_count(db: Session, user_id: int) -> int:
    return int(
        db.execute(
            select(func.count())
            .select_from(Notification)
            .where(Notification.user_id == user_id, Notification.is_read.is_(False))
        ).scalar_one()
    )


def list_notifications(
    db: Session, user_id: int, *, page: int, page_size: int
) -> tuple[list[Notification], int, int]:
    """分页取通知列表。返回 (当前页条目, 总条数, 未读条数)。"""
    total = int(
        db.execute(
            select(func.count())
            .select_from(Notification)
            .where(Notification.user_id == user_id)
        ).scalar_one()
    )
    unread = _unread_count(db, user_id)
    rows = (
        db.execute(
            select(Notification)
            .where(Notification.user_id == user_id)
            .order_by(Notification.sent_at.desc(), Notification.id.desc())
            .offset((page - 1) * page_size)
            .limit(page_size)
        )
        .scalars()
        .all()
    )
    return list(rows), total, unread


def unread_count(db: Session, user_id: int) -> int:
    """未读总数(首页铃铛角标)。"""
    return _unread_count(db, user_id)


def mark_read(
    db: Session, user_id: int, *, ids: list[int] | None = None, mark_all: bool = False
) -> tuple[int, int]:
    """标记已读。mark_all=True 全量清零,否则按 ids 逐条置读(不存在的 id 忽略,幂等)。

    返回 (本次实际由未读→已读的条数, 剩余未读数)。
    """
    if not mark_all:
        wanted = set(ids or [])
        if not wanted:
            return 0, _unread_count(db, user_id)

    stmt = select(Notification).where(
        Notification.user_id == user_id, Notification.is_read.is_(False)
    )
    if not mark_all:
        stmt = stmt.where(Notification.id.in_(wanted))

    now = datetime.now(timezone.utc)
    marked = 0
    for n in db.execute(stmt).scalars().all():
        n.is_read = True
        n.read_at = now
        marked += 1
    db.commit()
    return marked, _unread_count(db, user_id)


# ---------------------------------------------------------------------------
# 写:业务下发入口
# ---------------------------------------------------------------------------


def create_notification(
    db: Session,
    *,
    user_id: int,
    type_key: str,
    coins: int | None = None,
    cash_cents: int | None = None,
    info_rows: list[dict[str, str]] | None = None,
    extra: dict[str, str] | None = None,
    sent_at: datetime | None = None,
    dedup_key: str | None = None,
) -> Notification:
    """下发一条站内消息(业务事件统一入口)。type_key 必须是 catalog 的 13 类之一。

    dedup_key 非空时受部分唯一索引约束(同 user+type+dedup_key 未读期间仅一条);
    需要「同批次/同权限只保留一条未读」的调用方,应捕获 IntegrityError 或先查已存在的未读再决定
    更新 sent_at,而非重复插入(见 models/notification 的 uq_notification_user_type_dedup)。
    """
    catalog.get_type(type_key)  # 校验类型合法(未知类型抛 UnknownNotificationType)
    row = Notification(
        user_id=user_id,
        type=type_key,
        coins=coins,
        cash_cents=cash_cents,
        info_rows=info_rows or [],
        extra=extra or {},
        sent_at=sent_at or datetime.now(_CST),
        dedup_key=dedup_key,
    )
    db.add(row)
    db.commit()
    db.refresh(row)
    return row


# ---------------------------------------------------------------------------
# 样例内容(供 /push/test createNotification 联调;文案对齐 PRD §3)
# ---------------------------------------------------------------------------


def _card_reward_expiring(sent_at: datetime, coins: int = 86, cash: int = 1280, days: int = 3) -> dict:
    return {
        "coins": coins,
        "cash_cents": cash,
        "info_rows": [
            {
                "label": "过期说明",
                "value": f"您有{coins}金币和{cash_yuan(cash)}元现金即将失效，"
                         "完成一次一键领券或一键比价即可激活收益",
            },
            {"label": "过期时间", "value": f"{days}天后失效"},
        ],
        # batchId:同一批次激活成功后不再重复推送(PRD §2 激活逻辑)
        "extra": {"batchId": f"batch_{sent_at:%Y%m%d}"},
    }


def _card_reward_expired(sent_at: datetime, coins: int = 35, cash: int = 60) -> dict:
    return {
        "coins": coins,
        "cash_cents": cash,
        "info_rows": [
            {
                "label": "过期说明",
                "value": f"您的{coins}金币和{cash_yuan(cash)}元现金已失效，"
                         "完成一次一键领券或一键比价可赚取新收益",
            },
            {"label": "过期时间", "value": f"已过期 {sent_at.month}月{sent_at.day}日失效"},
        ],
        "extra": {},  # 点击跳赚钱页(tab),无需参数
    }


def _card_withdraw_success(sent_at: datetime, cash: int = 50) -> dict:
    return {
        "cash_cents": cash,
        "info_rows": [
            {"label": "到账账户", "value": "微信钱包"},
            {"label": "到账时间", "value": _fmt_time(sent_at)},
        ],
        "extra": {},  # 无跳转,仅消红点
    }


def _card_withdraw_failed(sent_at: datetime, cash: int = 350, reason: str = "微信零钱未实名") -> dict:
    return {
        "cash_cents": cash,
        "info_rows": [
            {"label": "失败原因", "value": reason},
            {"label": "退回说明", "value": "款项已原路退回现金余额"},
        ],
        "extra": {"withdrawId": "88001"},  # 点击跳提现页
    }


def _card_permission(permission: str) -> dict:
    # permission ∈ accessibility(无障碍)/ battery(省电策略)/ autostart(自启动)/ overlay(悬浮窗)
    # 客户端点击时按此 key 实时检测该权限并弹对应开启弹窗(PRD §2 权限逻辑)。
    return {
        "info_rows": [
            {"label": "说明文案", "value": "未开启将导致核心功能不可用，请尽快开启"},
        ],
        "extra": {"permission": permission},
    }


def _card_feedback_reply(feedback_id: str) -> dict:
    return {
        "info_rows": [
            {"label": "说明文案", "value": "快去看看官方给您的回复吧～"},
        ],
        "extra": {"feedbackId": feedback_id},  # 跳反馈历史页并滚动高亮该条(PRD §2)
    }


def _card_feedback_reward(sent_at: datetime, coins: int = 300,
                          reply: str = "感谢反馈，您说的问题已经修复上线，送您的金币请查收～") -> dict:
    return {
        "coins": coins,
        "info_rows": [
            {"label": "奖励说明", "value": "感谢您的反馈，您的金币奖励已到账"},
            {"label": "官方留言", "value": reply},  # PRD §3:官方留言必填(发奖励必带留言)
            {"label": "到账时间", "value": _fmt_time(sent_at)},
        ],
        "extra": {"feedbackId": "3002"},
    }


def _card_report_approved(sent_at: datetime, coins: int = 1000, store: str = "蜀大侠火锅") -> dict:
    return {
        "coins": coins,
        "info_rows": [
            {"label": "奖励说明", "value": f"您爆料的「{store}」更低价已通过审核，金币奖励已到账"},
            {"label": "到账时间", "value": _fmt_time(sent_at)},
        ],
        "extra": {"reportId": "5001"},  # 跳爆料记录页并滚动高亮该条
    }


def _card_invite_order_reward(sent_at: datetime, cash: int = 200, nickname: str = "柚子") -> dict:
    return {
        "cash_cents": cash,
        "info_rows": [
            {"label": "奖励说明", "value": f"好友「{nickname}」完成首次下单"},
            {"label": "到账时间", "value": _fmt_time(sent_at)},
        ],
        "extra": {"inviteeNickname": nickname},  # 跳邀请页(welfare/invite.html?from=notifications)
    }


def _card_invite_remind(nickname: str = "阿泽") -> dict:
    return {
        "info_rows": [
            {
                "label": "说明文案",
                "value": f"好友「{nickname}」已注册，还没完成比价下单，提醒TA完成后你可得2元现金",
            },
        ],
        # scrollTo=remind:跳邀请页并自动滚动到底部「提醒好友」模块(PRD §2 #13)
        "extra": {"inviteeNickname": nickname, "scrollTo": "remind"},
    }


def build_sample_card(type_key: str, sent_at: datetime | None = None) -> dict:
    """按类型生成一份样例卡片内容({coins?, cash_cents?, info_rows, extra}),/push/test 联调用。"""
    catalog.get_type(type_key)  # 校验 type 合法
    now = sent_at or datetime.now(_CST)
    builders = {
        "reward_expiring": lambda: _card_reward_expiring(now),
        "reward_expired": lambda: _card_reward_expired(now),
        "withdraw_success": lambda: _card_withdraw_success(now),
        "withdraw_failed": lambda: _card_withdraw_failed(now),
        "perm_accessibility": lambda: _card_permission("accessibility"),
        "perm_battery": lambda: _card_permission("battery"),
        "perm_autostart": lambda: _card_permission("autostart"),
        "perm_overlay": lambda: _card_permission("overlay"),
        "feedback_reply": lambda: _card_feedback_reply("3001"),
        "feedback_reward": lambda: _card_feedback_reward(now),
        "report_approved": lambda: _card_report_approved(now),
        "invite_order_reward": lambda: _card_invite_order_reward(now),
        "invite_remind": lambda: _card_invite_remind(),
    }
    return builders[type_key]()


def insert_sample(db: Session, user_id: int, type_key: str) -> Notification:
    """插入一条该类型的样例未读通知并落库(/push/test createNotification 联调:push extras 带上
    它的 id,客户端点击 push 后调 POST /notifications/read {ids:[id]} 即闭环验证已读联动)。"""
    return create_notification(db, user_id=user_id, type_key=type_key, **build_sample_card(type_key))
