"""照片上传 / 获取 / 删除 / 审核。

FEAT-008 avatar / FEAT-009 album。

存储：阿里云 OSS bucket，对象路径 `users/<user_id>/<purpose>/<photo_id>.<ext>`。
db 里 user_photos 表 + profiles.avatar_photo_id 引用。对外返回签名 URL（1h 有效）。

审核：上传同步调阿里云绿网（当前 stub：默认 'passed'，绿网 SDK 集成作为下阶段）。

配置（env 或 vault install 注入）：
    XIANGQIN_OSS_BUCKET     e.g. agentaily-xiangqin-photos-prod
    XIANGQIN_OSS_ENDPOINT   e.g. oss-cn-hangzhou.aliyuncs.com
    ALIYUN_ACCESS_KEY_ID    vault credential aliyun-main
    ALIYUN_ACCESS_KEY_SECRET
"""

from __future__ import annotations

import hashlib
import os
import sqlite3
from dataclasses import dataclass
from pathlib import Path

from . import db as db_module
from .db import new_id, now_ts

MAX_BYTES = 5 * 1024 * 1024  # 5 MB / 张
ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp"}
SIGNED_URL_TTL_SEC = 3600

_BUCKET = os.environ.get("XIANGQIN_OSS_BUCKET", "agentaily-xiangqin-photos-prod")
_ENDPOINT = os.environ.get("XIANGQIN_OSS_ENDPOINT", "oss-cn-hangzhou.aliyuncs.com")


class PhotoError(Exception):
    def __init__(self, code: str, message: str, http_status: int = 400):
        super().__init__(message)
        self.code = code
        self.http_status = http_status


@dataclass(frozen=True)
class Photo:
    id: str
    user_id: str
    purpose: str
    oss_key: str
    sha256: str
    content_type: str
    size_bytes: int
    uploaded_at: int
    moderation_status: str
    moderation_reason: str | None


def _resolve_db(path: Path | None) -> Path:
    # 动态读 db.DEFAULT_DB_PATH（conftest tmp_db fixture 通过改 module attr 注入测试路径）
    return path if path is not None else db_module.DEFAULT_DB_PATH


def _ext_from_content_type(ct: str) -> str:
    return {"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp"}[ct]


def _get_oss_bucket():
    """Lazy 拿 OSS bucket 客户端。测试时可 monkey-patch 此函数。"""
    ak_id = os.environ.get("ALIYUN_ACCESS_KEY_ID", "")
    ak_secret = os.environ.get("ALIYUN_ACCESS_KEY_SECRET", "")
    if not ak_id or not ak_secret:
        raise PhotoError(
            "oss_not_configured",
            "服务端未配 OSS 凭证（ALIYUN_ACCESS_KEY_ID/SECRET），照片功能不可用",
            http_status=503,
        )
    import oss2  # 延迟 import 方便测试

    auth = oss2.Auth(ak_id, ak_secret)
    return oss2.Bucket(auth, _ENDPOINT, _BUCKET)


def _moderate(data: bytes, content_type: str) -> tuple[str, str | None]:
    """内容审核 stub。

    下阶段接入阿里云绿网（alibabacloud-imageaudit）。当前永远返 passed。
    """
    _ = data, content_type
    return ("passed", None)


def upload(
    user_id: str,
    data: bytes,
    content_type: str,
    purpose: str,
    path: Path | None = None,
) -> Photo:
    """上传一张照片到 OSS + 登记 db。

    purpose: 'avatar' | 'album' | 'idcard'
    若 purpose='avatar' 会同步更新 profiles.avatar_photo_id（替换旧头像）。
    """
    if purpose not in ("avatar", "album", "idcard"):
        raise PhotoError("invalid_purpose", f"purpose 必须是 avatar/album/idcard，收到 {purpose}")
    if content_type not in ALLOWED_CONTENT_TYPES:
        raise PhotoError(
            "unsupported_type",
            f"只支持 jpeg/png/webp，收到 {content_type}",
        )
    if len(data) == 0:
        raise PhotoError("empty_file", "文件为空")
    if len(data) > MAX_BYTES:
        raise PhotoError("file_too_large", f"超过 {MAX_BYTES // 1024 // 1024} MB 上限")

    status, reason = _moderate(data, content_type)
    if status == "rejected":
        raise PhotoError("moderation_rejected", f"内容审核未通过: {reason}")

    photo_id = new_id()
    ext = _ext_from_content_type(content_type)
    oss_key = f"users/{user_id}/{purpose}/{photo_id}.{ext}"
    sha256 = hashlib.sha256(data).hexdigest()

    bucket = _get_oss_bucket()
    bucket.put_object(oss_key, data, headers={"Content-Type": content_type})

    ts = now_ts()
    with sqlite3.connect(_resolve_db(path)) as conn:
        conn.execute(
            """INSERT INTO user_photos
               (id, user_id, purpose, oss_key, sha256, content_type, size_bytes,
                uploaded_at, moderation_status, moderation_reason)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                photo_id,
                user_id,
                purpose,
                oss_key,
                sha256,
                content_type,
                len(data),
                ts,
                status,
                reason,
            ),
        )
        if purpose == "avatar":
            conn.execute(
                "UPDATE profiles SET avatar_photo_id=?, updated_at=? WHERE user_id=?",
                (photo_id, ts, user_id),
            )

    return Photo(
        id=photo_id,
        user_id=user_id,
        purpose=purpose,
        oss_key=oss_key,
        sha256=sha256,
        content_type=content_type,
        size_bytes=len(data),
        uploaded_at=ts,
        moderation_status=status,
        moderation_reason=reason,
    )


def signed_url(photo_id: str, path: Path | None = None) -> str | None:
    """按 photo_id 生成 OSS 签名下载 URL（TTL 1h）。不存在返 None。"""
    with sqlite3.connect(_resolve_db(path)) as conn:
        row = conn.execute("SELECT oss_key FROM user_photos WHERE id=?", (photo_id,)).fetchone()
    if not row:
        return None
    oss_key = row[0]
    bucket = _get_oss_bucket()
    return bucket.sign_url("GET", oss_key, SIGNED_URL_TTL_SEC)


def avatar_url(user_id: str, path: Path | None = None) -> str | None:
    """查某用户当前头像的签名 URL。profile 没配头像则返 None。"""
    with sqlite3.connect(_resolve_db(path)) as conn:
        row = conn.execute(
            "SELECT avatar_photo_id FROM profiles WHERE user_id=?", (user_id,)
        ).fetchone()
    if not row or not row[0]:
        return None
    return signed_url(row[0], path=path)


def list_album(user_id: str, path: Path | None = None) -> list[Photo]:
    """用户的相册（按上传时间倒序）。"""
    with sqlite3.connect(_resolve_db(path)) as conn:
        rows = conn.execute(
            """SELECT id, user_id, purpose, oss_key, sha256, content_type,
                      size_bytes, uploaded_at, moderation_status, moderation_reason
               FROM user_photos
               WHERE user_id=? AND purpose='album'
               ORDER BY uploaded_at DESC""",
            (user_id,),
        ).fetchall()
    return [Photo(*r) for r in rows]


def delete(user_id: str, photo_id: str, path: Path | None = None) -> None:
    """删一张照片：db + OSS。要求 photo 属于 user（防越权）。"""
    with sqlite3.connect(_resolve_db(path)) as conn:
        row = conn.execute(
            "SELECT oss_key, user_id, purpose FROM user_photos WHERE id=?",
            (photo_id,),
        ).fetchone()
        if not row:
            raise PhotoError("not_found", "photo 不存在", http_status=404)
        oss_key, owner, purpose = row
        if owner != user_id:
            raise PhotoError("forbidden", "不是你的 photo", http_status=403)
        # 删 db 记录
        conn.execute("DELETE FROM user_photos WHERE id=?", (photo_id,))
        # 如果是当前头像，清 profile 引用
        if purpose == "avatar":
            conn.execute(
                "UPDATE profiles SET avatar_photo_id=NULL, updated_at=? WHERE user_id=? AND avatar_photo_id=?",
                (now_ts(), user_id, photo_id),
            )
    # OSS 删对象（删完 db 再删 OSS；OSS 失败不阻塞 db）
    try:
        _get_oss_bucket().delete_object(oss_key)
    except Exception:
        pass  # OSS 残留对象成本很低，过期清理任务会处理
