"""
Bot API 操作审计中间件
======================

每次 Bot API 调用（/api/cma/bot*）记录一行 JSON 审计日志到
backend/logs/bot_audit.log（可用环境变量 CMA_BOT_AUDIT_LOG 覆盖路径）。

JSON 行字段: timestamp / bot_name / endpoint / method / risk_level /
            entity_id / status
L3 批量写额外记录 rows（导入行数 / 批量条数）。

设计约束:
  - 不阻塞业务: 所有日志写入失败仅静默跳过，不影响请求结果
  - 只读 body（starlette 会缓存并回放给下游），仅在 application/json 时解析
  - 不改动任何现有 API 行为与多租户隔离
"""
import json
import logging
import os
from datetime import datetime

from fastapi import Request

_LOG_DIR = os.path.join(
    os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
    "logs",
)
_DEFAULT_LOG_PATH = os.path.join(_LOG_DIR, "bot_audit.log")

_audit_logger = None


def _get_logger():
    """构造/复用审计 logger（路径取 CMA_BOT_AUDIT_LOG 覆盖值，便于测试隔离）"""
    global _audit_logger
    if _audit_logger is None:
        log_path = os.getenv("CMA_BOT_AUDIT_LOG") or _DEFAULT_LOG_PATH
        os.makedirs(os.path.dirname(log_path), exist_ok=True)
        logger = logging.getLogger("cma.bot_audit")
        logger.setLevel(logging.INFO)
        logger.propagate = False
        # 清掉旧 handler，避免日志名单例导致路径切换失效
        for h in list(logger.handlers):
            logger.removeHandler(h)
            try:
                h.close()
            except Exception:
                pass
        handler = logging.FileHandler(log_path, encoding="utf-8")
        handler.setFormatter(logging.Formatter("%(message)s"))
        logger.addHandler(handler)
        _audit_logger = logger
    return _audit_logger


def write_audit_line(record: dict):
    """写一行 JSON 审计日志；任何异常都静默（不阻塞业务）"""
    try:
        _get_logger().info(json.dumps(record, ensure_ascii=False))
    except Exception:
        pass


def _resolve_bot_name(request: Request) -> str:
    """解析调用方标识: X-BOT-KEY → 配置名/原始key；bridge → bot-bridge；其余 → web-user/unknown"""
    key = request.headers.get("X-BOT-KEY")
    if key:
        try:
            from app.api.bot_bridge import _BOT_API_KEYS
            info = _BOT_API_KEYS.get(key)
            if isinstance(info, dict) and info.get("name"):
                return info["name"]
        except Exception:
            pass
        return key
    if request.headers.get("X-BRIDGE-TOKEN"):
        return "bot-bridge"
    if (request.headers.get("Authorization") or "").startswith("Bearer "):
        return "web-user"
    return "unknown"


def _resolve_risk_level(request: Request, route=None, endpoint=None):
    """级别解析顺序: 路由函数装饰器标注 → API_RISK_MAP(route.path) → API_RISK_MAP(请求URL)"""
    if endpoint is not None:
        level = getattr(endpoint, "risk_level", None)
        if level:
            return level
    if route is not None:
        from app.risk_levels import get_risk_level
        path = getattr(route, "path", None)
        if path:
            for m in getattr(route, "methods", set()) or set():
                if m in ("GET", "POST", "PUT", "DELETE", "PATCH"):
                    level = get_risk_level(m, path)
                    if level:
                        return level
    try:
        from app.risk_levels import get_risk_level
        return get_risk_level(request.method, request.url.path)
    except Exception:
        return None


async def _extract_entity_id(request: Request):
    """多租户 entity_id 提取（尽力而为）: 查询参数 → JSON body（仅 application/json）"""
    try:
        q = request.query_params.get("entity_id")
        if q is not None and str(q) != "":
            return int(q) if str(q).isdigit() else q
    except Exception:
        pass
    ctype = (request.headers.get("content-type") or "").lower()
    if "application/json" in ctype:
        try:
            raw = await request.body()
            if raw:
                data = json.loads(raw)
                eid = data.get("entity_id")
                if eid is not None:
                    return int(eid) if str(eid).isdigit() else eid
        except Exception:
            pass
    return None


def _extract_l3_rows(level, resp_body: bytes):
    """L3 批量写: 从响应体提取行数/条数（imported / kpi_updated / causality_links / total）"""
    if level != "L3" or not resp_body:
        return None
    try:
        data = json.loads(resp_body)
    except Exception:
        return None
    if not isinstance(data, dict):
        return None
    for key in ("imported", "kpi_updated"):
        v = data.get(key)
        if isinstance(v, (int, float)):
            return int(v)
    links = data.get("causality_links")
    if isinstance(links, list):
        return len(links)
    total = data.get("total")
    if isinstance(total, (int, float)):
        return int(total)
    return None


async def bot_audit_middleware(request: Request, call_next):
    """HTTP 中间件: 仅审计 /api/cma/bot* 路径；任何异常不影响业务"""
    path = request.url.path
    if not path.startswith("/api/cma/bot"):
        return await call_next(request)

    entity_id = None
    try:
        entity_id = await _extract_entity_id(request)
    except Exception:
        entity_id = None

    try:
        response = await call_next(request)
    except Exception:
        # 兜底记录（全局异常handler会返回500，此处保证审计不丢）
        try:
            write_audit_line({
                "timestamp": datetime.now().isoformat(timespec="seconds"),
                "bot_name": _resolve_bot_name(request),
                "endpoint": path,
                "method": request.method,
                "risk_level": _resolve_risk_level(request) or "NA",
                "entity_id": entity_id,
                "status": 500,
            })
        except Exception:
            pass
        raise

    # 级别解析（route 由路由器在 call_next 内写入 scope）
    level = None
    try:
        route = request.scope.get("route")
        endpoint = getattr(route, "endpoint", None) if route else None
        level = _resolve_risk_level(request, route=route, endpoint=endpoint)
    except Exception:
        level = None

    # 捕获响应体（L3 需要行数），并重放
    resp_body = b""
    try:
        body_iterator = getattr(response, "body_iterator", None)
        if body_iterator is not None:
            chunks = [chunk async for chunk in body_iterator]
            resp_body = b"".join(chunks)
        rows = _extract_l3_rows(level, resp_body)
    except Exception:
        rows = None

    record = {
        "timestamp": datetime.now().isoformat(timespec="seconds"),
        "bot_name": _resolve_bot_name(request),
        "endpoint": path,
        "method": request.method,
        "risk_level": level or "NA",
        "entity_id": entity_id,
        "status": response.status_code,
    }
    if rows is not None:
        record["rows"] = rows

    try:
        write_audit_line(record)
    except Exception:
        pass

    # 响应体已被消费 → 重建响应（Bot接口均为小JSON，非流式）
    if resp_body:
        from fastapi.responses import Response
        return Response(
            content=resp_body,
            status_code=response.status_code,
            headers=dict(response.headers),
            media_type=response.media_type,
        )
    return response
