"""消息通知中心:业务事件 → 站内消息 + 厂商 push 的统一下发口。

PRD《消息通知中心》真实业务触发在此收口(替代 /push/test 的样例数据),已接入:
  #3  withdraw_success    提现到账(repositories/wallet 各「pending→success」转换点)
  #4  withdraw_failed     提现失败/退回(repositories/wallet._refund_withdraw,含审核拒绝)
  #9  feedback_reply      官方回复(admin 反馈审核「拒绝」,带用户可见原因/留言)
  #10 feedback_reward     反馈奖励(admin 反馈审核「采纳」发金币,必带官方留言)
  #11 report_approved     爆料审核通过(admin 上报更低价「通过」发金币)
  #12 invite_order_reward 好友下单到账(repositories/invite.try_reward_on_compare 发奖后)

行为约定(调用方唯一需要知道的两条):
  1. **绝不抛异常**——通知只是业务的副产物,站内消息落库失败/推送失败只 log,
     绝不让提现退款、审核发奖等主流程回滚或报错。
  2. **必须在业务事务 commit 之后调用**——内部会再 commit(写 notification 表);
     若在业务半途调用,会把调用方未提交的脏状态一并提交。

去重:各事件用业务主键做 dedup_key(提现单号/反馈 id/爆料 id/被邀请人 id),配合
notification 表的部分唯一索引,同一事件并发重复触发时未读期间只落一条、只推一次。

推送:向该用户所有已上报厂商 token 的设备直推(integrations/vendor_push);
厂商凭据未配置(本地/测试环境)时自动跳过推送、只落站内消息。extras 按
PRD §4 约定带 {type, notificationId, ...跳转参数},客户端点击 push 深链落地
并调 POST /notifications/read 同步置读。
"""
from __future__ import annotations

import logging
from datetime import datetime
from typing import TYPE_CHECKING

from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.core import notification_catalog as catalog
from app.core.rewards import CN_TZ
from app.integrations import vendor_push
from app.models.user import User
from app.repositories import device as device_repo
from app.repositories import notification as notif_repo

if TYPE_CHECKING:
    from app.models.feedback import Feedback
    from app.models.notification import Notification
    from app.models.price_report import PriceReport
    from app.models.wallet import WithdrawOrder

logger = logging.getLogger("shagua.notification_events")


def _fmt_time(dt: datetime) -> str:
    """信息行「到账时间」的展示格式(与 repositories/notification 样例卡一致)。"""
    return dt.strftime("%Y-%m-%d %H:%M")


def _yuan_trim(cents: int) -> str:
    """分 → 元,去掉多余的 0(200→"2"、1280→"12.80")。push 正文用(PRD §5 示例口径:
    「{2}元现金已到账」);卡片数值仍走 cash_cents 由前端按两位小数渲染。"""
    yuan = cents / 100
    return f"{yuan:.2f}".rstrip("0").rstrip(".")


def _display_name(user: User | None) -> str:
    """好友昵称展示:昵称 → 微信昵称 → 手机尾号,全无则「好友」。"""
    name = ((user.nickname if user else None) or (user.wechat_nickname if user else None) or "").strip()
    if not name and user and user.phone:
        name = f"用户{user.phone[-4:]}"
    return name or "好友"


# ---------------------------------------------------------------------------
# 内核:落站内消息 + 厂商推送(全程吞异常)
# ---------------------------------------------------------------------------


def _dispatch(
    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,
    dedup_key: str | None = None,
    push_vars: dict[str, str] | None = None,
) -> Notification | None:
    """落一条站内消息并向该用户设备直推。返回落库行;去重命中/失败返回 None。"""
    try:
        row = notif_repo.create_notification(
            db,
            user_id=user_id,
            type_key=type_key,
            coins=coins,
            cash_cents=cash_cents,
            info_rows=info_rows,
            extra=extra,
            dedup_key=dedup_key,
        )
    except IntegrityError:
        # 同 (user, type, dedup_key) 已有未读消息 = 同一事件并发/重复触发 → 不重复落、不重复推
        db.rollback()
        logger.info(
            "notification dedup hit user_id=%s type=%s dedup_key=%s", user_id, type_key, dedup_key
        )
        return None
    except Exception:  # noqa: BLE001 — 通知失败绝不影响业务主流程
        logger.exception("create notification failed user_id=%s type=%s", user_id, type_key)
        try:
            db.rollback()
        except Exception:  # noqa: BLE001 — 回滚失败也不外抛,session 由请求生命周期兜底
            logger.exception("rollback after notification failure also failed")
        return None

    _push_to_user_devices(db, row, push_vars)
    return row


def _push_to_user_devices(db: Session, row: Notification, push_vars: dict[str, str] | None) -> None:
    """向消息归属用户的全部厂商推送目标直推(best-effort,单设备失败不影响其余)。"""
    try:
        title, body = catalog.render_push(row.type, push_vars)
        # PRD §4 push 联动:extras 至少带 type + notificationId,外加该类型的跳转参数(extra 列)
        extras: dict[str, str] = {"type": row.type}
        extras.update({str(k): str(v) for k, v in (row.extra or {}).items()})
        extras["notificationId"] = str(row.id)

        for dev in device_repo.list_push_targets(db, user_id=row.user_id):
            vendor = vendor_push.normalize_vendor(dev.push_vendor)
            if not vendor or vendor not in vendor_push.SUPPORTED_VENDORS:
                continue
            if vendor_push.missing_settings(vendor):
                # 本地/测试环境凭据不齐 → 只落站内消息,不发真推送(与 push/vendors 的报缺口径一致)
                logger.info(
                    "skip push (vendor %s not configured) user_id=%s type=%s",
                    vendor, row.user_id, row.type,
                )
                continue
            try:
                response = vendor_push.send_notification(
                    vendor, dev.push_token, title=title, body=body, extras=extras
                )
                if vendor == "huawei" and row.type in {"feedback_reply", "feedback_reward"}:
                    # 华为反馈推送联调日志：保留厂商返回码/requestId 等排障信息，
                    # 请求本身的 token、Authorization 和应用密钥不会进入 response。
                    logger.info(
                        "huawei feedback push response user_id=%s type=%s "
                        "notification_id=%s response=%s",
                        row.user_id, row.type, row.id, response,
                    )
                logger.info(
                    "push sent user_id=%s type=%s vendor=%s notification_id=%s",
                    row.user_id, row.type, vendor, row.id,
                )
            except vendor_push.VendorPushError as e:
                logger.warning(
                    "push failed user_id=%s type=%s vendor=%s: %s", row.user_id, row.type, vendor, e
                )
            try:
                vendor_push.send_data_event(
                    vendor,
                    dev.push_token,
                    event=vendor_push.DATA_EVENT_NOTIFICATION_CREATED,
                    notification_id=str(row.id),
                )
            except vendor_push.VendorPushError as e:
                # 透传只负责前台铃铛实时刷新，失败不能影响通知栏消息或站内消息。
                logger.warning(
                    "push data event failed user_id=%s type=%s vendor=%s: %s",
                    row.user_id, row.type, vendor, e,
                )
    except Exception:  # noqa: BLE001 — 渲染/查设备等意外失败同样不外抛
        logger.exception("push notification failed user_id=%s type=%s", row.user_id, row.type)


# ---------------------------------------------------------------------------
# 六个业务事件(PRD §1/§3/§5 编号见文件头)
# ---------------------------------------------------------------------------


def notify_withdraw_success(db: Session, order: WithdrawOrder) -> None:
    """#3 提现成功:款项已存入微信零钱。点击无跳转仅消红点(extra 空)。"""
    _dispatch(
        db,
        user_id=order.user_id,
        type_key="withdraw_success",
        cash_cents=order.amount_cents,
        info_rows=[
            {"label": "到账账户", "value": "微信钱包"},
            {"label": "到账时间", "value": _fmt_time(datetime.now(CN_TZ))},
        ],
        extra={},
        dedup_key=order.out_bill_no,
        push_vars={"amount": notif_repo.cash_yuan(order.amount_cents)},
    )


def notify_withdraw_failed(db: Session, order: WithdrawOrder) -> None:
    """#4 提现失败/退回:含微信侧失败、审核拒绝、解绑退回。点击跳提现页重新提现。

    失败原因用 order.fail_reason(与 /withdraw/status 下发的用户可读原因同源)。
    """
    reason = (order.fail_reason or "").strip() or "提现未成功"
    _dispatch(
        db,
        user_id=order.user_id,
        type_key="withdraw_failed",
        cash_cents=order.amount_cents,
        info_rows=[
            {"label": "失败原因", "value": reason},
            {"label": "退回说明", "value": "款项已原路退回现金余额"},
        ],
        extra={"withdrawId": order.out_bill_no},
        dedup_key=order.out_bill_no,
        push_vars={"amount": notif_repo.cash_yuan(order.amount_cents), "reason": reason},
    )


def notify_feedback_reply(db: Session, feedback: Feedback) -> None:
    """#9 官方回复:运营审核了反馈且未采纳(用户可见原因/留言落在反馈记录上)。
    点击跳反馈历史页滚动高亮该条(extra.feedbackId)。"""
    _dispatch(
        db,
        user_id=feedback.user_id,
        type_key="feedback_reply",
        info_rows=[{"label": "说明文案", "value": "快去看看官方给您的回复吧～"}],
        extra={"feedbackId": str(feedback.id)},
        dedup_key=str(feedback.id),
    )


def notify_feedback_reward(db: Session, feedback: Feedback) -> None:
    """#10 反馈奖励:反馈被采纳,金币已到账。PRD 约定发奖必带官方留言(admin_reply);
    运营漏填时省略该信息行,不硬造文案。"""
    coins = int(feedback.reward_coins or 0)
    info_rows = [{"label": "奖励说明", "value": "感谢您的反馈，您的金币奖励已到账"}]
    reply = (feedback.admin_reply or "").strip()
    if reply:
        info_rows.append({"label": "官方留言", "value": reply})
    info_rows.append({"label": "到账时间", "value": _fmt_time(datetime.now(CN_TZ))})
    _dispatch(
        db,
        user_id=feedback.user_id,
        type_key="feedback_reward",
        coins=coins,
        info_rows=info_rows,
        extra={"feedbackId": str(feedback.id)},
        dedup_key=str(feedback.id),
        push_vars={"coins": str(coins)},
    )


def notify_report_approved(db: Session, report: PriceReport) -> None:
    """#11 爆料审核通过:上报的更低价过审,金币已到账。点击跳爆料记录页高亮该条。"""
    coins = int(report.reward_coins or 0)
    store = (report.store_name or "").strip() or "该店铺"
    _dispatch(
        db,
        user_id=report.user_id,
        type_key="report_approved",
        coins=coins,
        info_rows=[
            {"label": "奖励说明", "value": f"您爆料的「{store}」更低价已通过审核，金币奖励已到账"},
            {"label": "到账时间", "value": _fmt_time(datetime.now(CN_TZ))},
        ],
        extra={"reportId": str(report.id)},
        dedup_key=str(report.id),
        push_vars={"store": store, "coins": str(coins)},
    )


def notify_invite_order_reward(
    db: Session, *, inviter_user_id: int, invitee_user_id: int, cash_cents: int
) -> None:
    """#12 好友下单到账:被邀请好友完成首次下单(比价),现金奖励已入邀请人账户。
    通知发给【邀请人】;每个好友只发一次奖 → dedup 按被邀请人。"""
    invitee = db.get(User, invitee_user_id)
    nickname = _display_name(invitee)
    _dispatch(
        db,
        user_id=inviter_user_id,
        type_key="invite_order_reward",
        cash_cents=cash_cents,
        info_rows=[
            {"label": "奖励说明", "value": f"好友「{nickname}」完成首次下单"},
            {"label": "到账时间", "value": _fmt_time(datetime.now(CN_TZ))},
        ],
        extra={"inviteeNickname": nickname},
        dedup_key=str(invitee_user_id),
        push_vars={"nickname": nickname, "amount": _yuan_trim(cash_cents)},
    )
