# Copyright (C) 2026 GoodData Corporation

"""Log helpers for extracting summaries and errors from persisted logs."""

from __future__ import annotations

import hashlib
import re
from pathlib import Path

from aida_mcp.output.policy import (
    LOG_TAIL_MAX_BYTES,
    LOG_TAIL_MAX_LINES,
    MAX_ERROR_LINE_CHARS,
    MAX_ERROR_LINES,
    MAX_LOG_LINE_CHARS,
)
from aida_mcp.utils.artifacts import get_artifacts_root

_ERROR_PATTERNS = [
    re.compile(r"FAILURE:"),
    re.compile(r"BUILD FAILED"),
    re.compile(r"\bERROR\b", re.IGNORECASE),
    re.compile(r"\bFAILED\b", re.IGNORECASE),
    re.compile(r"Exception", re.IGNORECASE),
    re.compile(r"Traceback", re.IGNORECASE),
    re.compile(r"Caused by:", re.IGNORECASE),
    re.compile(r"Unauthorized", re.IGNORECASE),
    re.compile(r"\b401\b"),
    re.compile(r"\b403\b"),
    re.compile(r"\b404\b"),
    re.compile(r"status code", re.IGNORECASE),
    re.compile(r"Could not GET", re.IGNORECASE),
    re.compile(r"Could not get resource", re.IGNORECASE),
    re.compile(r"Could not resolve", re.IGNORECASE),
]


def read_log_tail(
    log_path: str | Path | None,
    *,
    max_bytes: int = LOG_TAIL_MAX_BYTES,
    max_lines: int = LOG_TAIL_MAX_LINES,
) -> list[str]:
    if not log_path:
        return []
    path = Path(log_path)
    if not path.exists():
        return []

    try:
        with path.open("rb") as handle:
            handle.seek(0, 2)
            size = handle.tell()
            seek_back = min(size, max_bytes)
            handle.seek(size - seek_back)
            data = handle.read()
        text = data.decode("utf-8", errors="replace")
    except OSError:
        return []

    lines = [_truncate_log_line(ln.rstrip("\n")) for ln in text.splitlines() if ln.strip()]
    if len(lines) > max_lines:
        lines = lines[-max_lines:]
    return lines


def extract_error_payload(
    log_path: str | Path | None,
    *,
    max_errors: int = MAX_ERROR_LINES,
) -> tuple[list[str], int, bool]:
    lines = read_log_tail(log_path)
    return extract_error_payload_from_lines(lines, max_errors=max_errors)


def extract_error_payload_from_lines(
    lines: list[str],
    *,
    max_errors: int = MAX_ERROR_LINES,
) -> tuple[list[str], int, bool]:
    if not lines:
        return [], 0, False

    matches = [_truncate_error_line(ln) for ln in lines if any(pat.search(ln) for pat in _ERROR_PATTERNS)]
    if not matches:
        return [], 0, False

    truncated = len(matches) > max_errors
    return matches[-max_errors:], len(matches), truncated


def _truncate_error_line(line: str, max_chars: int = MAX_ERROR_LINE_CHARS) -> str:
    return _truncate_line(line, max_chars)


def _truncate_log_line(line: str, max_chars: int = MAX_LOG_LINE_CHARS) -> str:
    return _truncate_line(line, max_chars)


def _truncate_line(line: str, max_chars: int) -> str:
    if max_chars <= 0:
        return ""
    if len(line) <= max_chars:
        return line
    artifact_path = _store_oversized_line(line)
    suffix = "..."
    if artifact_path:
        suffix = f"... (full line at {artifact_path})"
    if max_chars <= len(suffix):
        return suffix[:max_chars]
    prefix_len = max_chars - len(suffix)
    return f"{line[:prefix_len]}{suffix}"


def truncate_output_line(line: str, max_chars: int) -> str:
    """Truncate tool-output lines safely and store oversized content."""
    return _truncate_line(line, max_chars)


def _store_oversized_line(line: str) -> Path | None:
    try:
        root = get_artifacts_root() / "oversize-lines"
        root.mkdir(parents=True, exist_ok=True)
        digest = hashlib.sha256(line.encode("utf-8", errors="replace")).hexdigest()
        path = root / f"{digest}.log"
        if not path.exists():
            path.write_text(line, encoding="utf-8", errors="replace")
        return path
    except OSError:
        return None
