"""admin CPS 分发与对账:群/活动管理 + 生成落地页短链 + 联盟订单对账 + 统计。

平台:meituan(actId+sid 转链 + query_order 对账) / taobao(整段淘口令) / jd(链接)。
淘宝暂未接 API → 只统计点击(咱落地页 PV/UV + 淘宝复制),对账字段显示 "-"。
群/活动管理 = operator;订单对账(涉佣金) = finance;只读列表/统计 = 登录即可。
"""
from __future__ import annotations

import time
from datetime import date as _date, datetime, time as _dt_time, timedelta, timezone
from typing import Annotated

from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile

from app.admin.audit import write_audit
from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role
from app.admin.repositories import cps as cps_repo
from app.admin.schemas.common import CursorPage
from app.admin.schemas.cps import (
    CpsActivityCreate,
    CpsActivityOut,
    CpsActivityUpdate,
    CpsGroupCreate,
    CpsGroupOut,
    CpsGroupStat,
    CpsGroupUpdate,
    CpsOrderOut,
    CpsReconcileResult,
    CpsReferralLinkItem,
    CpsReferralLinksOut,
    CpsReferralLinksRequest,
    CpsStatsOut,
)
from app.core import media
from app.core.config import settings
from app.integrations import meituan
from app.integrations.jd_union import JdUnionError
from app.integrations.meituan import MeituanCpsError
from app.models.admin import AdminUser
from app.models.cps_activity import CpsActivity
from app.models.cps_group import CpsGroup
from app.repositories import cps_link as cps_link_repo

router = APIRouter(
    prefix="/admin/api/cps",
    tags=["admin-cps"],
    dependencies=[Depends(get_current_admin)],
)

_VALID_PLATFORMS = {"meituan", "taobao", "jd"}


# ───────────── 群 ─────────────
@router.get("/groups", response_model=CursorPage[CpsGroupOut], summary="群列表")
def list_groups(
    db: AdminDb,
    keyword: Annotated[str | None, Query(max_length=100)] = None,
    status: Annotated[str | None, Query(pattern="^(active|archived)$")] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    cursor: Annotated[int | None, Query()] = None,
) -> CursorPage[CpsGroupOut]:
    items, next_cursor, total = cps_repo.list_groups(
        db, keyword=keyword, status=status, limit=limit, cursor=cursor
    )
    return CursorPage(
        items=[CpsGroupOut.model_validate(g) for g in items],
        next_cursor=next_cursor,
        total=total,
    )


@router.post("/groups", response_model=CpsGroupOut, summary="新建群")
def create_group(
    body: CpsGroupCreate,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> CpsGroupOut:
    bad = [p for p in body.platforms if p not in _VALID_PLATFORMS]
    if bad:
        raise HTTPException(status_code=400, detail=f"非法平台: {bad}")
    has_meituan = "meituan" in body.platforms
    if has_meituan and body.sid and cps_repo.get_group_by_sid(db, body.sid) is not None:
        raise HTTPException(status_code=409, detail="sid 已存在")
    group = cps_repo.create_group(
        db, name=body.name, platforms=body.platforms, sid=body.sid,
        member_count=body.member_count, remark=body.remark, commit=False,
    )
    write_audit(
        db, admin, action="cps.group.create", target_type="cps_group", target_id=group.id,
        detail={"name": group.name, "platforms": group.platforms, "sid": group.sid},
        ip=get_client_ip(request), commit=False,
    )
    db.commit()
    db.refresh(group)
    return CpsGroupOut.model_validate(group)


@router.patch("/groups/{group_id}", response_model=CpsGroupOut, summary="编辑群")
def update_group(
    group_id: int,
    body: CpsGroupUpdate,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> CpsGroupOut:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    if body.platforms is not None:
        bad = [p for p in body.platforms if p not in _VALID_PLATFORMS]
        if bad:
            raise HTTPException(status_code=400, detail=f"非法平台: {bad}")
    cps_repo.update_group(
        db, group, name=body.name, platforms=body.platforms,
        member_count=body.member_count, status=body.status, remark=body.remark, commit=False,
    )
    write_audit(
        db, admin, action="cps.group.update", target_type="cps_group", target_id=group_id,
        detail=body.model_dump(exclude_none=True), ip=get_client_ip(request), commit=False,
    )
    db.commit()
    db.refresh(group)
    return CpsGroupOut.model_validate(group)


@router.delete("/groups/{group_id}", summary="删除群")
def delete_group(
    group_id: int,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> dict:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    write_audit(
        db, admin, action="cps.group.delete", target_type="cps_group", target_id=group_id,
        detail={"name": group.name, "sid": group.sid, "platforms": list(group.platforms or [])},
        ip=get_client_ip(request), commit=False,
    )
    cps_repo.delete_group(db, group, commit=False)
    db.commit()
    return {"ok": True}


# ───────────── 活动 ─────────────
@router.get("/activities", response_model=CursorPage[CpsActivityOut], summary="活动列表")
def list_activities(
    db: AdminDb,
    platform: Annotated[str | None, Query(max_length=20)] = None,
    status: Annotated[str | None, Query(pattern="^(active|archived)$")] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    cursor: Annotated[int | None, Query()] = None,
) -> CursorPage[CpsActivityOut]:
    items, next_cursor, total = cps_repo.list_activities(
        db, platform=platform, status=status, limit=limit, cursor=cursor
    )
    return CursorPage(
        items=[CpsActivityOut.model_validate(a) for a in items],
        next_cursor=next_cursor,
        total=total,
    )


@router.post("/activities", response_model=CpsActivityOut, summary="新建活动")
def create_activity(
    body: CpsActivityCreate,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> CpsActivityOut:
    if body.platform == "meituan":
        if not body.act_id and not body.product_view_sign:
            raise HTTPException(status_code=400, detail="美团活动需填 actId 或 productViewSign")
    else:  # taobao / jd
        if not body.payload:
            label = "淘口令" if body.platform == "taobao" else "推广链接"
            raise HTTPException(status_code=400, detail=f"{body.platform} 活动需填{label}")
        if body.platform == "taobao" and not body.image_url:
            raise HTTPException(status_code=400, detail="淘宝活动需提供落地页图(上传或选已有)")
    activity = cps_repo.create_activity(
        db, name=body.name, platform=body.platform, act_id=body.act_id,
        product_view_sign=body.product_view_sign, payload=body.payload,
        image_url=media.to_abs_media_url(body.image_url), remark=body.remark, commit=False,
    )
    write_audit(
        db, admin, action="cps.activity.create", target_type="cps_activity", target_id=activity.id,
        detail={"name": activity.name, "platform": activity.platform},
        ip=get_client_ip(request), commit=False,
    )
    db.commit()
    db.refresh(activity)
    return CpsActivityOut.model_validate(activity)


@router.patch("/activities/{activity_id}", response_model=CpsActivityOut, summary="编辑活动")
def update_activity(
    activity_id: int,
    body: CpsActivityUpdate,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> CpsActivityOut:
    activity = cps_repo.get_activity(db, activity_id)
    if activity is None:
        raise HTTPException(status_code=404, detail="活动不存在")
    # 用「合并后最终值」校验平台必填项(同新建口径),允许只改部分字段
    platform = body.platform or activity.platform
    act_id = body.act_id if body.act_id is not None else activity.act_id
    pvs = body.product_view_sign if body.product_view_sign is not None else activity.product_view_sign
    payload = body.payload if body.payload is not None else activity.payload
    image_url = body.image_url if body.image_url is not None else activity.image_url
    if platform == "meituan":
        if not act_id and not pvs:
            raise HTTPException(status_code=400, detail="美团活动需填 actId 或 productViewSign")
    else:  # taobao / jd
        if not payload:
            label = "淘口令" if platform == "taobao" else "推广链接"
            raise HTTPException(status_code=400, detail=f"{platform} 活动需填{label}")
        if platform == "taobao" and not image_url:
            raise HTTPException(status_code=400, detail="淘宝活动需提供落地页图(上传或选已有)")
    cps_repo.update_activity(
        db, activity, name=body.name, platform=body.platform, act_id=body.act_id,
        product_view_sign=body.product_view_sign, payload=body.payload,
        image_url=media.to_abs_media_url(body.image_url) if body.image_url else None,
        remark=body.remark, status=body.status, commit=False,
    )
    write_audit(
        db, admin, action="cps.activity.update", target_type="cps_activity", target_id=activity_id,
        detail=body.model_dump(exclude_none=True), ip=get_client_ip(request), commit=False,
    )
    db.commit()
    db.refresh(activity)
    return CpsActivityOut.model_validate(activity)


@router.post("/upload-image", summary="上传活动落地页图(返回绝对 URL)")
async def upload_activity_image(
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    file: UploadFile = File(..., description="落地页图(jpeg/png/webp,≤5MB)"),
) -> dict:
    data = await file.read()
    try:
        url = media.save_cps_image(admin.id, data)
    except media.MediaError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    return {"url": media.to_abs_media_url(url)}


@router.get("/activity-images", summary="已有活动落地页图(供新建复用选择)")
def activity_images(db: AdminDb) -> dict:
    return {"images": cps_repo.list_activity_images(db)}


@router.delete("/activities/{activity_id}", summary="删除活动")
def delete_activity(
    activity_id: int,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> dict:
    activity = cps_repo.get_activity(db, activity_id)
    if activity is None:
        raise HTTPException(status_code=404, detail="活动不存在")
    write_audit(
        db, admin, action="cps.activity.delete", target_type="cps_activity", target_id=activity_id,
        detail={"name": activity.name, "platform": activity.platform},
        ip=get_client_ip(request), commit=False,
    )
    cps_repo.delete_activity(db, activity, commit=False)
    db.commit()
    return {"ok": True}


# ───────────── 生成链接(批量,按平台分支) ─────────────
def _gen_one_link(db, group: CpsGroup, activity: CpsActivity):
    """给一个活动生成一条落地页 link。美团:转链拿短链(需群 sid);淘宝/京东:用 payload。"""
    if activity.platform == "meituan":
        if not group.sid:
            raise HTTPException(status_code=400, detail=f"群「{group.name}」无 sid,无法生成美团链接")
        try:
            resp = meituan.get_referral_link(
                act_id=activity.act_id, product_view_sign=activity.product_view_sign,
                sid=group.sid, link_type_list=[1, 2, 3],
            )
        except MeituanCpsError as e:
            raise HTTPException(status_code=502, detail=f"美团转链失败: {e}") from e
        link_map = {str(k): v for k, v in (resp.get("referralLinkMap") or {}).items()}
        if not link_map and resp.get("data"):
            link_map = {"1": resp["data"]}
        target = link_map.get("2") or link_map.get("1") or link_map.get("3")
        if not target:
            raise HTTPException(status_code=502, detail=f"美团未返回有效链接(活动「{activity.name}」)")
    else:  # taobao(淘口令) / jd(链接)
        if not activity.payload:
            raise HTTPException(status_code=400, detail=f"活动「{activity.name}」缺少内容")
        target = activity.payload
    return cps_link_repo.create_link(
        db, group_id=group.id, activity_id=activity.id, sid=group.sid,
        target_url=target, platform=activity.platform, commit=False,
    )


@router.post("/referral-links", response_model=CpsReferralLinksOut, summary="批量生成落地页短链")
def generate_referral_links(
    body: CpsReferralLinksRequest,
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("operator"))],
    db: AdminDb,
) -> CpsReferralLinksOut:
    group = cps_repo.get_group(db, body.group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    group_platforms = set(group.platforms or [])
    base = settings.CPS_REDIRECT_BASE.rstrip("/")

    items: list[CpsReferralLinkItem] = []
    for aid in body.activity_ids:
        activity = cps_repo.get_activity(db, aid)
        if activity is None:
            raise HTTPException(status_code=404, detail=f"活动 {aid} 不存在")
        if activity.platform not in group_platforms:
            raise HTTPException(
                status_code=400,
                detail=f"活动「{activity.name}」平台({activity.platform})不在群「{group.name}」范围内",
            )
        link = _gen_one_link(db, group, activity)
        items.append(CpsReferralLinkItem(
            activity_id=activity.id, activity_name=activity.name, platform=activity.platform,
            redirect_url=f"{base}/c/{link.code}" if base else f"/c/{link.code}", code=link.code,
        ))

    write_audit(
        db, admin, action="cps.referral_link.generate", target_type="cps_group", target_id=group.id,
        detail={"group": group.name, "activity_ids": body.activity_ids, "count": len(items)},
        ip=get_client_ip(request), commit=False,
    )
    db.commit()
    return CpsReferralLinksOut(group_name=group.name, results=items)


# ───────────── 订单对账 ─────────────
_BEIJING = timezone(timedelta(hours=8))


def _parse_day(value: str | None, *, field: str) -> _date | None:
    if value is None:
        return None
    try:
        return _date.fromisoformat(value)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=f"{field} 需为 YYYY-MM-DD") from e


def _reconcile_range_to_ts(
    date_from: _date | None, date_to: _date | None, days: int
) -> tuple[int, int]:
    if date_from is None and date_to is None:
        now = int(time.time())
        return now - days * 86400, now

    start_day = date_from or date_to
    end_day = date_to or date_from
    if start_day is None or end_day is None:
        raise HTTPException(status_code=422, detail="日期参数不完整")
    if start_day > end_day:
        start_day, end_day = end_day, start_day
    if (end_day - start_day).days + 1 > 90:
        raise HTTPException(status_code=422, detail="美团订单查询最长 90 天")

    start_dt = datetime.combine(start_day, _dt_time.min, tzinfo=_BEIJING)
    end_dt = datetime.combine(end_day + timedelta(days=1), _dt_time.min, tzinfo=_BEIJING)
    return int(start_dt.timestamp()), int(end_dt.timestamp())


def _reconcile_range_to_bj_dt(
    date_from: _date | None, date_to: _date | None, days: int
) -> tuple[datetime, datetime]:
    start_ts, end_ts = _reconcile_range_to_ts(date_from, date_to, days)
    return (
        datetime.fromtimestamp(start_ts, tz=_BEIJING),
        datetime.fromtimestamp(end_ts, tz=_BEIJING),
    )


def _merge_reconcile_result(total: dict, current: dict) -> None:
    for key in ("fetched", "inserted", "updated", "pages"):
        total[key] = int(total.get(key, 0)) + int(current.get(key, 0))


@router.post("/orders/reconcile", response_model=CpsReconcileResult, summary="拉取联盟订单对账")
def reconcile_orders(
    request: Request,
    admin: Annotated[AdminUser, Depends(require_role("finance"))],
    db: AdminDb,
    date_from: Annotated[str | None, Query(description="起始日 YYYY-MM-DD")] = None,
    date_to: Annotated[str | None, Query(description="结束日 YYYY-MM-DD")] = None,
    days: Annotated[int, Query(ge=1, le=90, description="未传日期时默认拉近 N 天")] = 7,
    sid: Annotated[str | None, Query(max_length=64)] = None,
    query_time_type: Annotated[int, Query(ge=1, le=3)] = 1,
    platform: Annotated[str, Query(pattern="^(all|meituan|jd)$")] = "all",
) -> CpsReconcileResult:
    parsed_from = _parse_day(date_from, field="date_from")
    parsed_to = _parse_day(date_to, field="date_to")
    result = {"fetched": 0, "inserted": 0, "updated": 0, "pages": 0}
    try:
        if platform in {"all", "meituan"}:
            start_ts, end_ts = _reconcile_range_to_ts(parsed_from, parsed_to, days)
            mt_result = cps_repo.reconcile_orders(
                db,
                start_time=start_ts,
                end_time=end_ts,
                query_time_type=query_time_type if query_time_type in (1, 2) else 2,
                sid=sid,
            )
            _merge_reconcile_result(result, mt_result)
        if platform in {"all", "jd"}:
            if sid:
                raise HTTPException(status_code=422, detail="京东订单刷新不支持 sid 筛选")
            start_dt, end_dt = _reconcile_range_to_bj_dt(parsed_from, parsed_to, days)
            jd_result = cps_repo.reconcile_jd_orders(
                db,
                start_time=start_dt,
                end_time=end_dt,
                query_time_type=query_time_type,
            )
            _merge_reconcile_result(result, jd_result)
    except MeituanCpsError as e:
        raise HTTPException(status_code=502, detail=f"美团拉单失败: {e}") from e
    except JdUnionError as e:
        raise HTTPException(status_code=502, detail=f"京东拉单失败: {e}") from e
    write_audit(
        db, admin, action="cps.order.reconcile", target_type="cps_order", target_id=None,
        detail={
            "platform": platform,
            "date_from": date_from,
            "date_to": date_to,
            "days": days,
            "sid": sid,
            "query_time_type": query_time_type,
            **result,
        },
        ip=get_client_ip(request),
        commit=True,
    )
    return CpsReconcileResult(**result)


@router.get("/orders", response_model=CursorPage[CpsOrderOut], summary="订单明细")
def list_orders(
    db: AdminDb,
    sid: Annotated[str | None, Query(max_length=64)] = None,
    mt_status: Annotated[str | None, Query(max_length=8)] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    cursor: Annotated[int | None, Query()] = None,
) -> CursorPage[CpsOrderOut]:
    items, next_cursor, total = cps_repo.list_orders(
        db, sid=sid, mt_status=mt_status, limit=limit, cursor=cursor
    )
    return CursorPage(
        items=[CpsOrderOut.model_validate(o) for o in items],
        next_cursor=next_cursor,
        total=total,
    )


# ───────────── 统计 ─────────────
@router.get("/stats", response_model=CpsStatsOut, summary="按群对账统计")
def get_stats(
    db: AdminDb,
    days: Annotated[int, Query(ge=1, le=90)] = 30,
) -> CpsStatsOut:
    date_to = datetime.now(timezone.utc)
    date_from = date_to - timedelta(days=days)
    rows = cps_repo.group_stats(db, date_from=date_from, date_to=date_to)
    stats = [CpsGroupStat(**r) for r in rows]
    return CpsStatsOut(
        groups=stats,
        total_order_count=sum(s.order_count or 0 for s in stats),
        total_est_commission_cents=sum(s.est_commission_cents or 0 for s in stats),
        total_settled_commission_cents=sum(s.settled_commission_cents or 0 for s in stats),
    )


@router.get("/groups/{group_id}/timeseries", summary="群点击时序(折线图:天/小时级 PV/UV/复制)")
def group_timeseries(
    group_id: int,
    db: AdminDb,
    granularity: Annotated[str, Query(pattern="^(day|hour)$")] = "day",
    days: Annotated[int, Query(ge=1, le=30)] = 7,
    date: Annotated[str | None, Query()] = None,  # hour 粒度看哪天(YYYY-MM-DD 北京),默认今天
) -> dict:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    bj = timezone(timedelta(hours=8))
    now_bj = datetime.now(bj)
    if granularity == "hour":
        if date:
            try:
                day0 = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=bj)
            except ValueError as e:
                raise HTTPException(status_code=400, detail="date 格式应为 YYYY-MM-DD") from e
        else:
            day0 = now_bj
        start = day0.replace(hour=0, minute=0, second=0, microsecond=0)
        end = start + timedelta(days=1) - timedelta(microseconds=1)
    else:
        start = (now_bj - timedelta(days=days - 1)).replace(hour=0, minute=0, second=0, microsecond=0)
        end = now_bj
    points = cps_repo.group_click_timeseries(
        db, group_id=group_id, granularity=granularity, start=start, end=end
    )
    return {
        "group_id": group.id,
        "group_name": group.name,
        "granularity": granularity,
        "points": points,
    }


@router.get("/groups/{group_id}/daily", summary="群每天明细大表格(点击+订单按天合并)")
def group_daily(
    group_id: int,
    db: AdminDb,
    days: Annotated[int, Query(ge=1, le=90)] = 7,
) -> dict:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    bj = timezone(timedelta(hours=8))
    now_bj = datetime.now(bj)
    start = (now_bj - timedelta(days=days - 1)).replace(hour=0, minute=0, second=0, microsecond=0)
    end = now_bj
    # 点击侧:连续 N 天补零,顺序 = start..end(与下面 while 同序,按 index 对齐避免跨年 MM-DD 碰撞)
    click_points = cps_repo.group_click_timeseries(
        db, group_id=group_id, granularity="day", start=start, end=end
    )
    # 订单侧:仅美团群(有 sid)有对账;淘宝/京东订单列返回 None(前端显示 "-")
    is_meituan = "meituan" in (group.platforms or []) and bool(group.sid)
    order_daily = (
        cps_repo.group_order_daily(db, sid=group.sid, start=start, end=end) if is_meituan else {}
    )
    _ORDER_KEYS = (
        "order_count", "canceled_count", "gmv_cents",
        "est_commission_cents", "refund_profit_cents", "settled_commission_cents",
    )
    rows = []
    cur = start.astimezone(bj).date()
    last = end.astimezone(bj).date()
    idx = 0
    while cur <= last:
        cp = click_points[idx] if idx < len(click_points) else None
        row = {
            "date": cur.strftime("%Y-%m-%d"),
            "click_pv": cp["click_pv"] if cp else 0,
            "click_uv": cp["click_uv"] if cp else 0,
            "copy_pv": cp["copy_pv"] if cp else 0,
        }
        if is_meituan:
            od = order_daily.get(cur.strftime("%Y-%m-%d"))
            row.update({k: (od[k] if od else 0) for k in _ORDER_KEYS})
        else:
            row.update({k: None for k in _ORDER_KEYS})
        rows.append(row)
        idx += 1
        cur += timedelta(days=1)
    return {
        "group_id": group.id,
        "group_name": group.name,
        "is_meituan": is_meituan,
        "days": days,
        "rows": rows,
    }


@router.get("/groups/{group_id}/wx-users", summary="群内微信用户(领券画像:头像/昵称/领券次数)")
def group_wx_users(group_id: int, db: AdminDb) -> dict:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    return {"users": cps_repo.group_wx_users(db, group_id=group_id)}


@router.get("/groups/{group_id}/day-users", summary="某天该群按用户的领券/点击 + 每人点过的券")
def group_day_users(
    group_id: int,
    db: AdminDb,
    date: Annotated[str, Query(description="北京日期 YYYY-MM-DD")],
) -> dict:
    group = cps_repo.get_group(db, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail="群不存在")
    bj = timezone(timedelta(hours=8))
    try:
        day0 = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=bj)
    except ValueError as e:
        raise HTTPException(status_code=400, detail="date 格式应为 YYYY-MM-DD") from e
    start = day0.replace(hour=0, minute=0, second=0, microsecond=0)
    end = start + timedelta(days=1)
    users = cps_repo.group_day_users(db, group_id=group_id, start=start, end=end)
    return {
        "group_id": group.id,
        "group_name": group.name,
        "date": date,
        "users": users,
    }
