# -*- coding: utf-8 -*-
"""结构化日志系统

记录 abyss CLI 所有关键事件，支持双写（JSONL + 纯文本）、级别过滤、
trace_id 串联、按类别分文件、旧日志 gzip 压缩。

文件组织（~/.abyss/logs/）：
  - abyss_YYYY-MM-DD.log + abyss_YYYY-MM-DD.jsonl     主日志（全量）
  - error_YYYY-MM-DD.log + error_YYYY-MM-DD.jsonl     错误日志（仅 WARN/ERROR）
  - metrics_YYYY-MM-DD.log + metrics_YYYY-MM-DD.jsonl 指标日志（API/TOOL 性能）
  - abyss_YYYY-MM-DD.log.gz                           昨天日志 gzip 压缩

向后兼容：保留所有原 EventLogger 方法签名 + 模块级转发函数。
"""
import os
import json
import time
import gzip
import logging
import threading
import uuid
import glob as _glob
from datetime import datetime, timedelta
from pathlib import Path

# ---------------------------------------------------------------------------
# 配置（可通过 set_config 运行时修改）
# ---------------------------------------------------------------------------
LOG_DIR = Path.home() / ".abyss" / "logs"
LOG_RETENTION_DAYS = 30
MAX_PAYLOAD_LEN = 2000
LOG_LEVEL = "INFO"  # DEBUG / INFO / WARN / ERROR

# ---------------------------------------------------------------------------
# 级别定义
# ---------------------------------------------------------------------------
LEVELS = {"DEBUG": 10, "INFO": 20, "WARN": 30, "ERROR": 40}
LEVEL_NAMES = {10: "DEBUG", 20: "INFO", 30: "WARN", 40: "ERROR"}

# 类别 → 默认级别映射（可被 _write 的 level 参数覆盖）
_CATEGORY_LEVEL = {
    "SESSION": "INFO",
    "USER_INPUT": "INFO",
    "SLASH_CMD": "INFO",
    "API": "INFO",
    "TOOL": "INFO",
    "BATCH": "DEBUG",
    "STREAM": "DEBUG",
    "SPINNER": "DEBUG",
    "SIGNAL": "WARN",
    "ERROR": "ERROR",
    "WARNING": "WARN",
    "DEBUG": "DEBUG",
    "INFO": "INFO",
    # P4 新增类别
    "HOOK": "INFO",
    "CONTEXT": "INFO",
    "AGENT": "INFO",
    "COMMAND": "INFO",
}

# 指标类别事件白名单（写入 metrics 文件）
_METRICS_EVENTS = {
    ("API", "response"), ("API", "first_token"), ("API", "retry"),
    ("API", "error"), ("API", "token_usage"),
    ("TOOL", "end"), ("TOOL", "correction"),
}

# ---------------------------------------------------------------------------
# trace_id / session_id 上下文（线程隔离）
# ---------------------------------------------------------------------------
_trace_ctx = threading.local()
_session_ctx = threading.local()


def new_trace_id() -> str:
    """生成新 trace_id 并绑定到当前线程，返回 id。"""
    tid = uuid.uuid4().hex[:8]
    _trace_ctx.trace_id = tid
    return tid


def get_trace_id() -> str:
    return getattr(_trace_ctx, "trace_id", "")


def clear_trace_id() -> None:
    _trace_ctx.trace_id = ""


def set_session_id(sid: str) -> None:
    _session_ctx.session_id = sid or ""


def get_session_id() -> str:
    return getattr(_session_ctx, "session_id", "")


# ---------------------------------------------------------------------------
# StructuredLogger
# ---------------------------------------------------------------------------
class StructuredLogger:
    """结构化日志记录器。

    特性：
    - 双写：纯文本 .log（人眼友好）+ JSONL .jsonl（机器可解析）
    - 级别过滤：DEBUG/INFO/WARN/ERROR，低于 set_level 的不写入
    - trace_id 串联：一次用户输入 → 多轮 API/TOOL 事件共享同一 trace_id
    - 文件分离：主日志（全量）/ error（WARN+ERROR）/ metrics（性能指标）
    - 线程安全：_rotate_if_needed 在锁内，避免 OSError
    - 旧日志 gzip 压缩：昨天的 .log/.jsonl 自动压缩为 .gz
    - 沙箱降级：写入失败时禁用日志，不阻塞主流程
    """

    def __init__(self):
        self._lock = threading.Lock()
        self._date = None
        self._files = {}  # {"main": (log_f, jsonl_f), "error": ..., "metrics": ...}
        self._level = LEVELS.get(LOG_LEVEL, 20)
        self._disabled = False  # 沙箱降级标志
        try:
            LOG_DIR.mkdir(parents=True, exist_ok=True)
        except OSError:
            self._disabled = True

    # ---- 配置 ----
    def set_level(self, level: str) -> None:
        """设置日志级别（DEBUG/INFO/WARN/ERROR）。"""
        self._level = LEVELS.get(level.upper(), 20)

    def get_level(self) -> str:
        return LEVEL_NAMES.get(self._level, "INFO")

    # ---- 文件轮转 ----
    def _rotate_if_needed(self):
        """必须在 self._lock 内调用。按天轮转 + 压缩昨天 + 清理过期。"""
        today = datetime.now().strftime("%Y-%m-%d")
        if self._date == today and self._files:
            return
        # 关闭旧文件
        for log_f, jsonl_f in self._files.values():
            try:
                log_f.close()
                jsonl_f.close()
            except Exception:
                pass
        self._date = today
        try:
            self._files = {
                "main": (
                    open(str(LOG_DIR / f"abyss_{today}.log"), "a", encoding="utf-8", buffering=1),
                    open(str(LOG_DIR / f"abyss_{today}.jsonl"), "a", encoding="utf-8", buffering=1),
                ),
                "error": (
                    open(str(LOG_DIR / f"error_{today}.log"), "a", encoding="utf-8", buffering=1),
                    open(str(LOG_DIR / f"error_{today}.jsonl"), "a", encoding="utf-8", buffering=1),
                ),
                "metrics": (
                    open(str(LOG_DIR / f"metrics_{today}.log"), "a", encoding="utf-8", buffering=1),
                    open(str(LOG_DIR / f"metrics_{today}.jsonl"), "a", encoding="utf-8", buffering=1),
                ),
            }
        except OSError:
            self._disabled = True
            self._files = {}
            return
        self._cleanup_old()
        self._compress_old()

    def _cleanup_old(self):
        """清理超过保留期的日志文件（含 .gz）。"""
        cutoff = datetime.now() - timedelta(days=LOG_RETENTION_DAYS)
        for fp in _glob.glob(str(LOG_DIR / "*.log")) + \
                _glob.glob(str(LOG_DIR / "*.jsonl")) + \
                _glob.glob(str(LOG_DIR / "*.gz")):
            try:
                fname = os.path.basename(fp)
                date_part = self._extract_date(fname)
                if date_part and datetime.strptime(date_part, "%Y-%m-%d") < cutoff:
                    os.remove(fp)
            except (ValueError, OSError):
                pass

    @staticmethod
    def _extract_date(fname: str) -> str:
        """从文件名提取日期部分。支持 abyss_/error_/metrics_ 前缀 + .log/.jsonl/.gz 后缀。"""
        for prefix in ("abyss_", "error_", "metrics_"):
            if fname.startswith(prefix):
                rest = fname[len(prefix):]
                for suffix in (".jsonl.gz", ".log.gz", ".jsonl", ".log"):
                    if rest.endswith(suffix):
                        return rest[:-len(suffix)]
        return ""

    def _compress_old(self):
        """压缩昨天的日志为 .gz（仅未压缩的，避免重复）。"""
        yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        for prefix in ("abyss_", "error_", "metrics_"):
            for ext in (".log", ".jsonl"):
                fp = LOG_DIR / f"{prefix}{yesterday}{ext}"
                if fp.exists():
                    try:
                        gz_path = str(fp) + ".gz"
                        with open(fp, "rb") as src, gzip.open(gz_path, "wb") as dst:
                            dst.write(src.read())
                        os.remove(fp)
                    except OSError:
                        pass

    # ---- 核心写入 ----
    def _write(self, category: str, event: str, payload: dict, level: str = None):
        """核心写入方法。

        Args:
            category: 事件类别（SESSION/API/TOOL/ERROR/...）
            event: 事件名（start/end/response/...）
            payload: 详情字段字典
            level: 显式级别，None 时按 _CATEGORY_LEVEL 查
        """
        if self._disabled:
            return
        lvl = level or _CATEGORY_LEVEL.get(category, "INFO")
        lvl_num = LEVELS.get(lvl, 20)
        if lvl_num < self._level:
            return  # 级别过滤

        with self._lock:
            try:
                self._rotate_if_needed()
                if not self._files:
                    return
            except Exception:
                self._disabled = True
                return

            ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.") + \
                f"{int(time.time() * 1000) % 1000:03d}"

            # 纯文本行（向后兼容老格式，加 level 字段）
            payload_str = self._fmt(**payload)
            text_line = f"{ts} | {lvl:5s} | {category:11s} | {event:24s} | {payload_str}\n"

            # JSONL 行
            json_obj = {
                "ts": ts,
                "level": lvl,
                "cat": category,
                "event": event,
            }
            trace_id = get_trace_id()
            session_id = get_session_id()
            if trace_id:
                json_obj["trace_id"] = trace_id
            if session_id:
                json_obj["session_id"] = session_id
            for k, v in payload.items():
                json_obj[k] = self._truncate(v)
            jsonl_line = json.dumps(json_obj, ensure_ascii=False, default=str) + "\n"

            try:
                # 主日志（全量）
                self._write_to("main", text_line, jsonl_line)
                # 错误日志（WARN+）
                if lvl_num >= LEVELS["WARN"]:
                    self._write_to("error", text_line, jsonl_line)
                # 指标日志（性能事件）
                if (category, event) in _METRICS_EVENTS:
                    self._write_to("metrics", text_line, jsonl_line)
            except (OSError, ValueError):
                # 写入失败（沙箱/fd 损坏），降级禁用
                self._disabled = True

    def _write_to(self, kind: str, text_line: str, jsonl_line: str):
        log_f, jsonl_f = self._files[kind]
        log_f.write(text_line)
        log_f.flush()
        jsonl_f.write(jsonl_line)
        jsonl_f.flush()

    # ---- 格式化辅助 ----
    def _truncate(self, v):
        """截断长字段，用于 JSONL。"""
        if isinstance(v, str) and len(v) > MAX_PAYLOAD_LEN:
            return v[:MAX_PAYLOAD_LEN] + f"...[{len(v)}]"
        if isinstance(v, (dict, list)):
            s = json.dumps(v, ensure_ascii=False, default=str)
            if len(s) > MAX_PAYLOAD_LEN:
                return s[:MAX_PAYLOAD_LEN] + f"...[{len(s)}]"
        return v

    def _fmt(self, **kwargs) -> str:
        """格式化为 key=value 空格分隔（纯文本 payload）。"""
        parts = []
        for k, v in kwargs.items():
            if isinstance(v, str) and len(v) > MAX_PAYLOAD_LEN:
                v = v[:MAX_PAYLOAD_LEN] + f"...[{len(v)}]"
            elif isinstance(v, (dict, list)):
                v = json.dumps(v, ensure_ascii=False, default=str)
                if len(v) > MAX_PAYLOAD_LEN:
                    v = v[:MAX_PAYLOAD_LEN] + f"...[{len(v)}]"
            parts.append(f"{k}={v}")
        return " ".join(parts)

    # ---- 向后兼容 API（原 EventLogger 方法）----
    def session_start(self, **kwargs):
        self._write("SESSION", "start", kwargs)

    def session_end(self, reason=""):
        self._write("SESSION", "end", {"reason": reason or "normal"})

    def user_input(self, text: str):
        self._write("USER_INPUT", "input", {"text": text})

    def slash_command(self, cmd: str):
        self._write("SLASH_CMD", "exec", {"cmd": cmd})

    def api_request(self, **kwargs):
        self._write("API", "request_start", kwargs)

    def api_first_token(self, latency_ms: int):
        self._write("API", "first_token", {"latency_ms": latency_ms})

    def api_response(self, **kwargs):
        self._write("API", "response", kwargs)

    def api_error(self, error: str):
        self._write("API", "error", {"error": error}, level="ERROR")

    def api_retry(self, attempt: int, wait_s: float, reason: str):
        self._write("API", "retry", {"attempt": attempt, "wait_s": wait_s, "reason": reason}, level="WARN")

    def tool_start(self, name: str, **kwargs):
        self._write("TOOL", "start", {"name": name, **kwargs})

    def tool_end(self, name: str, success: bool, duration_ms: int, **kwargs):
        self._write("TOOL", "end", {
            "name": name, "success": success, "duration_ms": duration_ms, **kwargs
        })

    def spinner_start(self, message: str):
        self._write("SPINNER", "start", {"message": message}, level="DEBUG")

    def spinner_stop(self, message: str = ""):
        self._write("SPINNER", "stop", {"message": message}, level="DEBUG")

    def batch_header(self, round_num: int, total: int, max_rounds: int):
        self._write("BATCH", "header", {"round": round_num, "total": total, "max": max_rounds}, level="DEBUG")

    def stream_content_start(self):
        self._write("STREAM", "content_start", {}, level="DEBUG")

    def stream_reasoning_start(self):
        self._write("STREAM", "reasoning_start", {}, level="DEBUG")

    def keyboard_interrupt(self):
        self._write("SIGNAL", "keyboard_interrupt", {}, level="WARN")

    def eof(self):
        self._write("SIGNAL", "eof", {}, level="WARN")

    def error(self, message: str, exc_info: str = ""):
        payload = {"message": message}
        if exc_info:
            payload["traceback"] = exc_info
        self._write("ERROR", "", payload, level="ERROR")

    def warning(self, message: str):
        self._write("WARNING", "", {"message": message}, level="WARN")

    def debug(self, message: str):
        self._write("DEBUG", "", {"message": message}, level="DEBUG")

    def info(self, message: str):
        self._write("INFO", "", {"message": message}, level="INFO")

    def flush(self):
        with self._lock:
            for log_f, jsonl_f in self._files.values():
                try:
                    log_f.flush()
                    jsonl_f.flush()
                except Exception:
                    pass

    # ---- P4 新增事件方法（框架预留，后续逐步接入）----
    def tool_correction(self, tool_name: str, attempt: int, error: str, success: bool = False):
        """自纠错循环触发。"""
        self._write("TOOL", "correction", {
            "tool": tool_name, "attempt": attempt, "error": error, "resolved": success
        })

    def tool_track(self, tool_name: str, consecutive_errors: int, error_type: str = ""):
        """ToolCallTracker 状态变化（连续失败）。"""
        self._write("TOOL", "track", {
            "tool": tool_name, "consecutive_errors": consecutive_errors, "error_type": error_type
        }, level="WARN")

    def hook_block(self, hook: str, tool: str, reason: str):
        """PreToolUse 钩子阻断。"""
        self._write("HOOK", "block", {"hook": hook, "tool": tool, "reason": reason}, level="WARN")

    def hook_exec(self, hook: str, tool: str, duration_ms: int, success: bool = True):
        """PostToolUse 钩子执行。"""
        self._write("HOOK", "exec", {
            "hook": hook, "tool": tool, "duration_ms": duration_ms, "success": success
        })

    def token_usage(self, prompt_tokens: int, completion_tokens: int, total_tokens: int, **kwargs):
        """Token 使用量。"""
        self._write("API", "token_usage", {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": total_tokens,
            **kwargs,
        })

    def compress(self, before_msgs: int, after_msgs: int, before_tokens: int = 0, after_tokens: int = 0, mode: str = ""):
        """上下文压缩事件。"""
        self._write("CONTEXT", "compress", {
            "before_msgs": before_msgs, "after_msgs": after_msgs,
            "before_tokens": before_tokens, "after_tokens": after_tokens, "mode": mode,
        })

    def circuit_breaker(self, state: str, failure_count: int = 0, reason: str = ""):
        """断路器状态转换。"""
        self._write("API", "circuit_breaker", {
            "state": state, "failure_count": failure_count, "reason": reason
        }, level="WARN")

    def session_save(self, name: str, msgs: int = 0, tool_calls: int = 0, success: bool = True):
        """会话保存。"""
        self._write("SESSION", "save", {
            "name": name, "msgs": msgs, "tool_calls": tool_calls, "success": success
        })

    def command_use(self, command: str, args: str = ""):
        """斜杠命令使用。"""
        self._write("COMMAND", "use", {"command": command, "args": args})

    def subagent_run(self, task: str, duration_ms: int, success: bool, **kwargs):
        """子 Agent 执行。"""
        self._write("AGENT", "subagent_run", {
            "task": task, "duration_ms": duration_ms, "success": success, **kwargs
        })

    def curator(self, action: str, target: str, reason: str = ""):
        """Curator 守护进程决策。"""
        self._write("AGENT", "curator", {
            "action": action, "target": target, "reason": reason
        })


# ---------------------------------------------------------------------------
# 单例 + 模块级转发函数（向后兼容）
# ---------------------------------------------------------------------------
_logger = StructuredLogger()


def get_logger() -> StructuredLogger:
    return _logger


def set_level(level: str) -> None:
    _logger.set_level(level)


def session_start(**kwargs):
    _logger.session_start(**kwargs)


def session_end(reason=""):
    _logger.session_end(reason)


def user_input(text: str):
    _logger.user_input(text)


def slash_command(cmd: str):
    _logger.slash_command(cmd)


def api_request(**kwargs):
    _logger.api_request(**kwargs)


def api_first_token(latency_ms: int):
    _logger.api_first_token(latency_ms)


def api_response(**kwargs):
    _logger.api_response(**kwargs)


def api_error(error: str):
    _logger.api_error(error)


def api_retry(attempt: int, wait_s: float, reason: str):
    _logger.api_retry(attempt, wait_s, reason)


def tool_start(name: str, **kwargs):
    _logger.tool_start(name, **kwargs)


def tool_end(name: str, success: bool, duration_ms: int, **kwargs):
    _logger.tool_end(name, success, duration_ms, **kwargs)


def spinner_start(message: str):
    _logger.spinner_start(message)


def spinner_stop(message: str = ""):
    _logger.spinner_stop(message)


def batch_header(round_num: int, total: int, max_rounds: int):
    _logger.batch_header(round_num, total, max_rounds)


def stream_content_start():
    _logger.stream_content_start()


def stream_reasoning_start():
    _logger.stream_reasoning_start()


def keyboard_interrupt():
    _logger.keyboard_interrupt()


def eof():
    _logger.eof()


def error(message: str, exc_info: str = ""):
    _logger.error(message, exc_info)


def warning(message: str):
    _logger.warning(message)


def debug(message: str):
    _logger.debug(message)


def info(message: str):
    _logger.info(message)


def flush():
    _logger.flush()


# ---------------------------------------------------------------------------
# 标准 logging 桥接（修复 file_history/undo/cron_service 日志丢失）
# ---------------------------------------------------------------------------
class _StandardLoggingBridge(logging.Handler):
    """把标准 logging 记录转发到 StructuredLogger。

    修复 file_history.py / undo.py / cron_service.py 等模块使用
    logging.getLogger(__name__) 但无 handler 导致日志静默丢失的问题。
    """

    def emit(self, record):
        if _logger._disabled:
            return
        try:
            msg = record.getMessage()
            if record.exc_info:
                import traceback
                msg += "\n" + "".join(traceback.format_exception(*record.exc_info))
            # 附加模块名便于追溯
            if record.name and record.name != "root":
                msg = f"[{record.name}] {msg}"
            if record.levelno >= logging.ERROR:
                _logger.error(msg)
            elif record.levelno >= logging.WARNING:
                _logger.warning(msg)
            elif record.levelno >= logging.INFO:
                _logger.info(msg)
            else:
                _logger.debug(msg)
        except Exception:
            # 桥接失败不影响主流程
            pass


def configure_standard_logging() -> None:
    """配置标准 logging 桥接到项目 logger。在 main.py 启动时调用一次。

    效果：任何使用 `import logging; logger = logging.getLogger(__name__)`
    的模块，其日志都会自动转发到 StructuredLogger，不再静默丢失。
    """
    root = logging.getLogger()
    # 避免重复添加
    if not any(isinstance(h, _StandardLoggingBridge) for h in root.handlers):
        root.addHandler(_StandardLoggingBridge())
    # 全部捕获，由 StructuredLogger 做级别过滤
    root.setLevel(logging.DEBUG)
