import logging
import os
import time
import uuid as _uuid
from datetime import datetime, timedelta

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, Response
from pydantic import BaseModel, EmailStr, field_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.compat import UTC
from app.config import settings
from app.core.cache import cache
from app.core.permissions import get_current_user
from app.core.security import create_access_token, create_refresh_token, decode_token, hash_password, verify_password
from app.core.token_store import token_store
from app.services.email_service import send_email
from app.api.v1.captcha import consume_verify_token

logger = logging.getLogger(__name__)
from app.db import get_db
from app.models.user import LoginLog, Tenant, User, UserTenant
from app.schemas.auth import (
    AuthResponse,
    LoginRequest,
    RefreshRequest,
    RegisterRequest,
    TenantInfo,
    TokenResponse,
    UserProfile,
    validate_password_strength,
)

_LOGIN_LOCKOUT_KEY = "login_fail:{ip}"
_LOGIN_MAX_ATTEMPTS = 5
_LOGIN_LOCKOUT_TTL = 900  # 15 min
_REGISTER_LIMIT_KEY = "register:{ip}"
_REGISTER_MAX_ATTEMPTS = 6
_REGISTER_WINDOW = 3600  # 1 hour


async def _check_login_rate_limit(request: Request):
    """检查登录失败次数，超过阈值返回 429"""
    ip = request.client.host if request.client else "unknown"
    key = _LOGIN_LOCKOUT_KEY.format(ip=ip)
    val = await cache.get(key)
    if val and val.get("count", 0) >= _LOGIN_MAX_ATTEMPTS:
        raise HTTPException(
            status_code=429,
            detail=f"登录尝试过于频繁，请 {_LOGIN_LOCKOUT_TTL // 60} 分钟后再试",
        )
    return ip, key


async def _record_login_failure(ip: str, key: str):
    """记录一次登录失败"""
    val = await cache.get(key)
    count = (val.get("count", 0) if val else 0) + 1
    await cache.set(key, {"count": count}, ttl=_LOGIN_LOCKOUT_TTL)


async def _record_login_success(key: str):
    """登录成功后清除失败记录"""
    await cache.delete(key)

router = APIRouter()


def _build_security_alert_html(user_id: str, ip: str) -> str:
    now = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
    return f"""<div style="font-family:Arial;max-width:500px;margin:0 auto;padding:20px">
<h2 style="color:#d03050">⚠️ 安全告警：Token 被盗用</h2>
<p>系统检测到已失效的 Refresh Token 被再次使用，您的账号可能已被盗用。</p>
<table style="border-collapse:collapse;width:100%;margin:16px 0">
<tr><td style="padding:8px;border:1px solid #ddd;color:#666">用户 ID</td>
    <td style="padding:8px;border:1px solid #ddd;font-family:monospace;font-size:12px">{user_id}</td></tr>
<tr><td style="padding:8px;border:1px solid #ddd;color:#666">来源 IP</td>
    <td style="padding:8px;border:1px solid #ddd;font-family:monospace">{ip}</td></tr>
<tr><td style="padding:8px;border:1px solid #ddd;color:#666">时间</td>
    <td style="padding:8px;border:1px solid #ddd">{now}</td></tr>
</table>
<p style="color:#666;font-size:13px">系统已自动吊销所有 Refresh Token，用户下次使用需重新登录。无需额外操作。</p>
<hr style="border:none;border-top:1px solid #eee;margin:16px 0">
<p style="color:#999;font-size:11px">OncoLit 安全系统自动发送</p>
</div>"""


_reset_tokens_fallback: dict[str, dict] = {}


async def _store_reset_token(token: str, user_id: str, ttl_seconds: int = 3600) -> None:
    from app.core.cache import cache
    stored = await cache.set(f"reset:{token}", {"user_id": user_id}, ttl=ttl_seconds)
    if not stored:
        _reset_tokens_fallback[token] = {"user_id": user_id, "expires": time.time() + ttl_seconds}


async def _get_reset_token(token: str) -> dict | None:
    from app.core.cache import cache
    val = await cache.get(f"reset:{token}")
    if val:
        return val
    stored = _reset_tokens_fallback.get(token)
    if stored and stored["expires"] > time.time():
        return stored
    if stored:
        _reset_tokens_fallback.pop(token, None)
    return None


async def _del_reset_token(token: str) -> None:
    from app.core.cache import cache
    await cache.delete(f"reset:{token}")
    _reset_tokens_fallback.pop(token, None)


@router.post("/register", response_model=AuthResponse, summary="注册")
async def register(req: RegisterRequest, request: Request, response: Response, db: AsyncSession = Depends(get_db)):
    ip, lock_key = await _check_login_rate_limit(request)
    # 注册独立限速：同一 IP 1 小时内最多 6 次
    reg_key = _REGISTER_LIMIT_KEY.format(ip=ip)
    reg_val = await cache.get(reg_key)
    if reg_val and reg_val.get("count", 0) >= _REGISTER_MAX_ATTEMPTS:
        raise HTTPException(status_code=429, detail="注册过于频繁，请1小时后再试")
    # 验证码校验（支持逐步开启：register 接口前端可先不传 verify_token）
    if req.verify_token:
        if not await consume_verify_token(req.verify_token):
            raise HTTPException(status_code=400, detail="验证码错误或已过期")
    # 注册限速计数（不论成功失败，同一 IP 1h 最多 6 次请求）
    new_count = (reg_val.get("count", 0) if reg_val else 0) + 1
    await cache.set(reg_key, {"count": new_count}, ttl=_REGISTER_WINDOW)
    existing = await db.execute(select(User.id).where(User.email == req.email))
    if existing.scalar(): raise HTTPException(status_code=409, detail="If approved, a confirmation email will be sent")
    user = User(email=req.email, hashed_password=hash_password(req.password), display_name=req.display_name)
    db.add(user); await db.flush()
    slug = f"user-{user.id.hex[:12]}"
    tenant = Tenant(name=f"{req.display_name}的个人空间", slug=slug)
    db.add(tenant); await db.flush()
    db.add(UserTenant(user_id=user.id, tenant_id=tenant.id, role="owner", is_default=True))
    user.last_login_at = datetime.now(UTC)
    db.add(LoginLog(user_id=user.id, tenant_id=tenant.id, ip_address=request.client.host if request.client else None,
                     user_agent=(request.headers.get("user-agent") or "")[:500], success=True))
    await db.commit(); await db.refresh(user); await db.refresh(tenant)
    access_token = create_access_token(str(user.id), str(tenant.id), "owner", platform_role=user.platform_role)
    refresh_token, refresh_jti = create_refresh_token(str(user.id), str(tenant.id), "owner", platform_role=user.platform_role)
    await token_store.store_refresh(refresh_jti, str(user.id), str(tenant.id))
    cookie_secure = request.headers.get("x-forwarded-proto", request.url.scheme) == "https"
    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        samesite="lax",
        secure=cookie_secure,
        path="/api/v1/auth",
        max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 86400,
    )
    return AuthResponse(user=UserProfile(id=str(user.id), email=user.email, display_name=user.display_name, title=user.title, avatar_url=user.avatar_url, platform_role=user.platform_role), token=TokenResponse(access_token=access_token, refresh_token=refresh_token), tenants=[TenantInfo(id=str(tenant.id), name=tenant.name, slug=tenant.slug, plan_type=tenant.plan_type, role="owner")])


@router.post("/login", response_model=AuthResponse, summary="登录")
async def login(req: LoginRequest, request: Request, response: Response, db: AsyncSession = Depends(get_db)):
    # 频率限制
    ip, lock_key = await _check_login_rate_limit(request)
    result = await db.execute(select(User).where(User.email == req.email))
    user = result.scalar()
    if not user or not verify_password(req.password, user.hashed_password or ""):
        await _record_login_failure(ip, lock_key)
        db.add(LoginLog(user_id=user.id if user else None, ip_address=request.client.host if request.client else None,
                         user_agent=(request.headers.get("user-agent") or "")[:500], success=False, fail_reason="invalid_credentials"))
        await db.commit()
        raise HTTPException(status_code=401, detail="Invalid credentials")
    # 登录成功，清除失败记录
    await _record_login_success(lock_key)
    if not user.is_active: raise HTTPException(status_code=403, detail="Account disabled")
    ut_result = await db.execute(select(UserTenant, Tenant).join(Tenant).where(UserTenant.user_id == user.id))
    tenants = []; default_tenant_id, default_role = None, "viewer"
    for ut, t in ut_result:
        tenants.append(TenantInfo(id=str(t.id), name=t.name, slug=t.slug, plan_type=t.plan_type, role=ut.role))
        if ut.is_default: default_tenant_id, default_role = str(t.id), ut.role
    if not default_tenant_id and tenants: default_tenant_id, default_role = tenants[0].id, tenants[0].role
    # 兼容老用户：仍没有 tenant 时查找或创建个人 tenant
    if not default_tenant_id:
        personal = await db.execute(
            select(Tenant).where(Tenant.slug == f"user-{user.id.hex[:12]}")
        )
        t = personal.scalar()
        if t:
            default_tenant_id, default_role = str(t.id), "owner"
        else:
            t = Tenant(name=f"{user.display_name}的个人空间", slug=f"user-{user.id.hex[:12]}")
            db.add(t); await db.flush()
            db.add(UserTenant(user_id=user.id, tenant_id=t.id, role="owner", is_default=True))
            default_tenant_id, default_role = str(t.id), "owner"
        tenants.append(TenantInfo(id=default_tenant_id, name=t.name, slug=t.slug, plan_type=t.plan_type, role=default_role))
    user.last_login_at = datetime.now(UTC)
    db.add(LoginLog(user_id=user.id, tenant_id=_uuid.UUID(default_tenant_id) if default_tenant_id else None,
                     ip_address=request.client.host if request.client else None,
                     user_agent=(request.headers.get("user-agent") or "")[:500], success=True))
    await db.commit()
    access_token = create_access_token(str(user.id), default_tenant_id or "", default_role, platform_role=user.platform_role)
    refresh_token, refresh_jti = create_refresh_token(str(user.id), default_tenant_id or "", default_role, platform_role=user.platform_role)
    await token_store.store_refresh(refresh_jti, str(user.id), default_tenant_id or "")
    cookie_secure = request.headers.get("x-forwarded-proto", request.url.scheme) == "https"
    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        samesite="lax",
        secure=cookie_secure,
        path="/api/v1/auth",
        max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 86400,
    )
    return AuthResponse(user=UserProfile(id=str(user.id), email=user.email, display_name=user.display_name, title=user.title, avatar_url=user.avatar_url, platform_role=user.platform_role), token=TokenResponse(access_token=access_token, refresh_token=refresh_token), tenants=tenants)


@router.post("/refresh", response_model=TokenResponse, summary="刷新令牌")
async def refresh(request: Request, response: Response, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)):
    token = request.cookies.get("refresh_token")
    if not token:
        try:
            body = await request.json()
            token = body.get("refresh_token") if isinstance(body, dict) else None
        except Exception:
            token = None
    if not token:
        return TokenResponse(access_token="", refresh_token="")
    try:
        payload = decode_token(token)
        if payload.get("type") != "refresh":
            return TokenResponse(access_token="", refresh_token="")
        stored = await token_store.validate_refresh(payload.get("jti", ""))
        if not stored:
            jti = payload.get("jti", "")
            if jti and await token_store.is_consumed(jti):
                logger.warning("refresh_token_replay_detected", extra={"jti": jti, "user": payload.get("sub")})
                background_tasks.add_task(
                    send_email,
                    "326030087@qq.com",
                    "[安全告警] 检测到 Refresh Token 被盗用",
                    _build_security_alert_html(payload.get("sub", "unknown"), request.client.host if request.client else "unknown"),
                )
            return TokenResponse(access_token="", refresh_token="")
    except Exception:
        return TokenResponse(access_token="", refresh_token="")
    # 轮换 refresh token：作废旧 token，发放新 token
    old_jti = payload.get("jti", "")
    if old_jti:
        await token_store.revoke_refresh(old_jti)
        await token_store.mark_consumed(old_jti)
    role = payload.get("role", "viewer")
    # 向后兼容：旧 refresh token 携带 is_superuser，新 token 统一用 platform_role
    platform_role = payload.get("platform_role")
    if platform_role is None and payload.get("is_superuser"):
        platform_role = "platform_owner"
    tid = payload.get("tid", "")
    if not tid:
        ut = (await db.execute(
            select(UserTenant).where(
                UserTenant.user_id == _uuid.UUID(payload["sub"]),
                UserTenant.is_default == True,
            )
        )).scalar()
        if ut:
            tid = str(ut.tenant_id)
        else:
            ut = (await db.execute(
                select(UserTenant).where(UserTenant.user_id == _uuid.UUID(payload["sub"]))
            )).scalar()
            if ut:
                tid = str(ut.tenant_id)
            else:
                # 兼容老用户：创建个人 tenant
                t = Tenant(
                    name=f"用户{payload['sub'][:8]}的个人空间",
                    slug=f"user-{_uuid.UUID(payload['sub']).hex[:12]}",
                )
                db.add(t)
                await db.flush()
                db.add(UserTenant(
                    user_id=_uuid.UUID(payload["sub"]),
                    tenant_id=t.id, role="owner", is_default=True,
                ))
                tid = str(t.id)
    refresh_token, refresh_jti = create_refresh_token(payload["sub"], tid, role, platform_role=platform_role)
    await token_store.store_refresh(refresh_jti, payload["sub"], tid)
    cookie_secure = request.headers.get("x-forwarded-proto", request.url.scheme) == "https"
    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        samesite="lax",
        secure=cookie_secure,
        path="/api/v1/auth",
        max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 86400,
    )
    return TokenResponse(
        access_token=create_access_token(payload["sub"], tid, role, platform_role=platform_role),
        refresh_token=refresh_token,
    )


@router.post("/logout", summary="登出")
async def logout(request: Request, response: Response, user: dict = Depends(get_current_user)):
    """登出：黑名单 access token + 吊销所有 refresh token + 清除 cookie"""
    from app.core.token_store import token_store
    jti = user.get("jti", "")
    if jti:
        await token_store.blacklist(jti, ttl=900)
    uid = user.get("sub", "")
    if uid:
        await token_store.revoke_all_refresh_for_user(uid)
    cookie_secure = request.headers.get("x-forwarded-proto", request.url.scheme) == "https"
    response.delete_cookie("refresh_token", path="/api/v1/auth", secure=cookie_secure)
    return {"status": "logged_out"}


# ─── 密码重置 ───

class ForgotPasswordRequest(BaseModel):
    email: EmailStr

class ResetPasswordRequest(BaseModel):
    token: str
    new_password: str

    _check_password = field_validator("new_password")(validate_password_strength)

@router.post("/forgot-password", response_model=dict, summary="忘记密码")
async def forgot_password(req: ForgotPasswordRequest, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).where(User.email == req.email))
    user = result.scalar()
    if not user: return {"status": "sent", "message": "If the email exists, a reset link has been sent."}
    token = _uuid.uuid4().hex
    await _store_reset_token(token, str(user.id))
    base_url = settings.PUBLIC_BASE_URL
    reset_link = f"{base_url}/auth/reset-password?token={token}"
    # DEV: 无 SMTP_HOST → 直接返回重置链接
    if not os.environ.get("SMTP_HOST"):
        return {"status": "sent", "reset_link": reset_link,
                "message": "[DEV模式] 未配置SMTP，使用返回的 reset_link 即可重置密码"}
    background_tasks.add_task(_send_reset_email, user.email, user.display_name, reset_link)
    return {"status": "sent", "message": "If the email exists, a reset link has been sent."}

async def _send_reset_email(to_email: str, name: str, link: str):
    try:
        from app.services.email_service import send_email
        html = f"""<div style="font-family:Arial;max-width:500px;margin:0 auto;padding:20px"><h2 style="color:#1a5276">🔬 肿瘤科文献中心</h2><p>{name}，你好：</p><p>你请求了密码重置。请点击下方链接设置新密码（1小时内有效）：</p><div style="text-align:center;margin:24px 0"><a href="{link}" style="background:#1a5276;color:#fff;padding:12px 32px;border-radius:6px;text-decoration:none;font-size:14px">重置密码 →</a></div><p style="color:#999;font-size:12px">如果你没有请求密码重置，请忽略此邮件。</p></div>"""
        await send_email(to_email, "密码重置 - 肿瘤科文献中心", html)
    except Exception:
        import logging
        logging.getLogger(__name__).exception("send_reset_email_failed")

@router.post("/reset-password", response_model=dict, summary="重置密码")
async def reset_password(req: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
    stored = await _get_reset_token(req.token)
    if not stored: raise HTTPException(status_code=400, detail="Invalid or expired reset token")
    result = await db.execute(select(User).where(User.id == _uuid.UUID(stored["user_id"])))
    user = result.scalar()
    if not user: raise HTTPException(status_code=404)
    user.hashed_password = hash_password(req.new_password); await db.commit()
    await _del_reset_token(req.token)
    return {"status": "ok", "message": "Password reset successfully"}


class ProfileUpdate(BaseModel):
    display_name: str | None = None
    title: str | None = None
    phone: str | None = None
    notify_email: bool | None = None
    digest_frequency: str | None = None

@router.get("/me", response_model=UserProfile, summary="用户信息")
async def get_me(user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
    u = (await db.execute(select(User).where(User.id == _uuid.UUID(user["sub"])))).scalar()
    if not u: raise HTTPException(404)
    return UserProfile(id=str(u.id), email=u.email, display_name=u.display_name, title=u.title, avatar_url=u.avatar_url, platform_role=u.platform_role)


@router.get("/tenants", summary="用户租户列表")
async def get_my_tenants(user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
    """返回当前用户的租户列表，并标注当前使用的租户。"""
    u_id = _uuid.UUID(user["sub"])
    current_tid = user.get("tid")
    result = await db.execute(
        select(UserTenant, Tenant).join(Tenant).where(UserTenant.user_id == u_id)
    )
    tenants = []
    for ut, t in result:
        tenants.append(TenantInfo(
            id=str(t.id), name=t.name, slug=t.slug,
            plan_type=t.plan_type, role=ut.role,
        ))
    return {"tenants": tenants, "current_tenant_id": current_tid}

@router.put("/me", response_model=dict, summary="更新资料")
async def update_me(req: ProfileUpdate, user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
    u = (await db.execute(select(User).where(User.id == _uuid.UUID(user["sub"])))).scalar()
    if not u: raise HTTPException(404)
    if req.display_name is not None: u.display_name = req.display_name
    if req.title is not None: u.title = req.title
    if req.phone is not None: u.phone = req.phone
    if req.notify_email is not None: u.notify_email = req.notify_email
    if req.digest_frequency is not None: u.digest_frequency = req.digest_frequency
    await db.commit()
    return {"status": "updated"}


class DeleteAccountConfirm(BaseModel):
    confirm: bool = True  # 必须显式确认


@router.post("/deactivate", summary="注销账号")
async def deactivate_account(_confirm: DeleteAccountConfirm, user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
    """注销账号：软删除（30天冷静期，期间可联系管理员恢复）"""
    u = (await db.execute(select(User).where(User.id == _uuid.UUID(user["sub"])))).scalar()
    if not u: raise HTTPException(404)
    u.is_active = False
    u.updated_at = datetime.now(UTC)
    await db.commit()
    # 清除客户端 token
    return {"status": "deactivated", "message": "账号已注销。30天内可联系管理员恢复。感谢使用肿瘤科文献中心。"}
