"""会话日志目录：列文件与尾部读取（供调试 Aicd 与工具链）。"""

from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


def list_session_logs(
    logs_dir: Path,
    *,
    limit: int = 30,
    current_process_log: Path | None = None,
) -> dict[str, Any]:
    """按修改时间倒序列出 ``*.log``。"""
    if not logs_dir.is_dir():
        return {
            "ok": False,
            "error": f"logs directory does not exist: {logs_dir}",
        }
    lim = max(1, min(100, int(limit)))
    cur_resolved: Path | None = None
    if current_process_log is not None:
        try:
            cur_resolved = current_process_log.resolve()
        except OSError:
            cur_resolved = None
    files = [
        p
        for p in logs_dir.iterdir()
        if p.is_file() and p.suffix.lower() == ".log"
    ]
    files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    picked = files[:lim]
    rows: list[dict[str, Any]] = []
    for p in picked:
        try:
            st = p.stat()
            mtime = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat()
            size_b = int(st.st_size)
        except OSError:
            mtime = ""
            size_b = 0
        try:
            pres = p.resolve()
        except OSError:
            pres = p
        is_proc = bool(cur_resolved and pres == cur_resolved)
        rows.append(
            {
                "name": p.name,
                "path": str(pres),
                "size_bytes": size_b,
                "mtime_utc": mtime,
                "is_this_process": is_proc,
            }
        )
    out: dict[str, Any] = {
        "ok": True,
        "logs_dir": str(logs_dir.resolve()),
        "files": rows,
        "hint": "Rows with is_this_process=true match this Aicd process. Use read_tail with this_process:true or filename.",
    }
    if cur_resolved and not any(r.get("is_this_process") for r in rows):
        out["hint"] += (
            " Current process log not in this page—increase limit or use read_tail this_process:true."
        )
    return out


def read_session_log_tail(
    logs_dir: Path,
    *,
    explicit_path: Path | None = None,
    filename: str | None = None,
    latest: bool = False,
    max_lines: int = 200,
    max_bytes: int = 120_000,
    line_contains: str | None = None,
    line_regex: str | None = None,
) -> dict[str, Any]:
    """从日志文件末尾读取文本（按字节窗口再截断行数），可选按子串/正则过滤行。"""
    if not logs_dir.is_dir():
        return {
            "ok": False,
            "error": f"logs directory does not exist: {logs_dir}",
        }
    lc = (line_contains or "").strip() if line_contains else ""
    lr = (line_regex or "").strip() if line_regex else ""
    if lc and lr:
        return {
            "ok": False,
            "error": "use only one of line_contains or line_regex",
        }
    cre: re.Pattern[str] | None = None
    if lr:
        try:
            cre = re.compile(lr)
        except re.error as e:
            return {"ok": False, "error": f"invalid line_regex: {e}"}

    logs_resolved = logs_dir.resolve()
    target: Path | None = None
    if explicit_path is not None:
        try:
            cand = explicit_path.resolve()
        except OSError as e:
            return {"ok": False, "error": str(e)}
        if not cand.is_file():
            return {"ok": False, "error": f"not a file: {cand}"}
        try:
            cand.relative_to(logs_resolved)
        except ValueError:
            return {
                "ok": False,
                "error": "explicit_path must stay under aicd session logs_dir",
            }
        target = cand
    elif latest or not (filename or "").strip():
        files = [
            p
            for p in logs_dir.iterdir()
            if p.is_file() and p.suffix.lower() == ".log"
        ]
        if not files:
            return {"ok": False, "error": "no .log files in logs directory"}
        target = max(files, key=lambda p: p.stat().st_mtime)
    else:
        name = str(filename).strip().replace("\\", "/")
        if "/" in name or ".." in name:
            return {"ok": False, "error": "filename must be a basename only"}
        cand = (logs_dir / name).resolve()
        try:
            cand.relative_to(logs_resolved)
        except ValueError:
            return {"ok": False, "error": "path must stay under logs_dir"}
        if not cand.is_file():
            return {"ok": False, "error": f"not a file: {cand}"}
        target = cand

    assert target is not None
    ml = max(10, min(2000, int(max_lines)))
    mb = max(1024, min(500_000, int(max_bytes)))

    try:
        raw = _read_tail_bytes(target, mb)
    except OSError as e:
        return {"ok": False, "error": str(e)}
    text = raw.decode("utf-8", errors="replace")
    lines = text.splitlines()
    truncated_lines = len(lines) > ml
    if truncated_lines:
        lines = lines[-ml:]
    before_filter = len(lines)
    filter_note = ""
    if cre:
        lines_m = [ln for ln in lines if cre.search(ln)]
        filter_note = "line_regex"
        lines = lines_m
    elif lc:
        low = lc.lower()
        lines = [ln for ln in lines if low in ln.lower()]
        filter_note = "line_contains"

    body = "\n".join(lines)
    out: dict[str, Any] = {
        "ok": True,
        "path": str(target.resolve()),
        "tail_line_count": before_filter,
        "tail_truncated_by_lines": truncated_lines,
        "byte_window": mb,
        "text": body,
        "filtered_line_count": len(lines),
        "filter_applied": filter_note or None,
    }
    if filter_note and len(lines) == 0:
        out["hint"] = (
            "No lines matched in this tail window; raise max_bytes/max_lines or remove the filter."
        )
    return out


def _read_tail_bytes(path: Path, max_bytes: int) -> bytes:
    with path.open("rb") as f:
        f.seek(0, 2)
        size = f.tell()
        start = max(0, size - max_bytes)
        f.seek(start)
        return f.read()
