# Copyright (C) 2026 GoodData Corporation

"""Shared finalization for ValidationRunner-based tools.

This centralizes the common footer/timing handling and structured result creation.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast

import orjson

from aida_mcp.output.log_utils import extract_error_payload, truncate_output_line
from aida_mcp.output.policy import (
    MAX_ERROR_LINES,
    MAX_RESULT_BYTES,
    MAX_RESULT_CHARS,
    MAX_RESULT_LINE_CHARS,
)
from aida_mcp.tools.core.results import ValidationResult
from aida_mcp.tools.core.results.types import JsonDict
from aida_mcp.tools.core.runner import RunnerResult
from aida_mcp.utils.progress import ProgressTracker
from aida_mcp.utils.timing import StepTimer

if TYPE_CHECKING:
    from mcp.server.fastmcp import Context as MCPContext

    Context = MCPContext[Any, Any, Any]


def _truncate_result_text(text: str, *, max_chars: int) -> tuple[str, bool, int]:
    if len(text) <= max_chars:
        return text, False, 0
    marker = f"\n... (truncated {len(text) - max_chars} chars) ...\n"
    remaining = max_chars - len(marker)
    if remaining <= 0:
        return marker[:max_chars], True, len(text) - max_chars
    head_len = remaining // 2
    tail_len = remaining - head_len
    truncated = text[:head_len] + marker + text[-tail_len:]
    return truncated, True, len(text) - max_chars


def _ensure_payload_size(payload: dict[str, object], *, max_bytes: int) -> dict[str, object]:
    def payload_size(candidate: dict[str, object]) -> int | None:
        try:
            return len(orjson.dumps(candidate, option=orjson.OPT_NON_STR_KEYS))
        except (TypeError, ValueError):
            return None

    size = payload_size(payload)
    if size is None or size <= max_bytes:
        return payload

    reduced = dict(payload)
    if reduced.get("steps"):
        reduced["steps"] = []
        reduced["steps_truncated"] = True
        size = payload_size(reduced)
        if size is not None and size <= max_bytes:
            return reduced

    text = reduced.get("text")
    if isinstance(text, str) and text:
        base_payload = dict(reduced)
        base_payload["text"] = ""
        base_size = payload_size(base_payload) or 0
        available = max(max_bytes - base_size, 0)
        truncated_text, _, _ = _truncate_result_text(text, max_chars=available)
        truncation_meta = {
            "text": truncated_text,
            "text_truncated": True,
            "text_truncated_chars": len(text) - len(truncated_text),
            "text_max_chars": available,
            "text_max_bytes": max_bytes,
        }
        reduced.update(truncation_meta)
        size = payload_size(reduced)
        if size is not None and size <= max_bytes:
            return reduced

    payload_dict = cast(dict[str, Any], payload)
    log_paths = payload_dict.get("log_paths")
    errors = payload_dict.get("errors")
    error_count = payload_dict.get("error_count")
    steps = payload_dict.get("steps")

    step_dicts: list[dict[str, Any]] = []
    if isinstance(steps, list):
        steps_list = cast(list[object], steps)
        for step in steps_list:
            if isinstance(step, dict):
                step_dicts.append(cast(dict[str, Any], step))

    if not log_paths and step_dicts:
        step_logs: list[str] = []
        for step in step_dicts:
            log_path = step.get("log_path")
            if isinstance(log_path, str) and log_path:
                step_logs.append(log_path)
        if step_logs:
            log_paths = step_logs

    if errors is None and step_dicts:
        step_errors: list[str] = []
        for step in step_dicts:
            excerpt = step.get("error_excerpt")
            if isinstance(excerpt, str) and excerpt:
                step_errors.append(excerpt)
        if step_errors:
            errors = step_errors

    error_excerpt = None
    if isinstance(errors, list) and errors:
        errors_list_raw = cast(list[object], errors)
        errors_list = [str(line) for line in errors_list_raw if line is not None]
        excerpt_lines = [line for line in errors_list[-3:] if line]
        if excerpt_lines:
            error_excerpt = "\n".join(excerpt_lines)

    minimal: dict[str, Any] = {
        "tool": payload_dict.get("tool"),
        "success": payload_dict.get("success"),
        "steps": [],
        "text": "Result too large; see log_paths.",
        "text_truncated": True,
        "text_max_bytes": max_bytes,
    }
    if log_paths:
        minimal["log_paths"] = log_paths
    if error_excerpt:
        minimal["error_excerpt"] = error_excerpt
    if error_count is not None:
        minimal["error_count"] = error_count
    return minimal


async def finalize_runner_validation(
    *,
    tool: str,
    header_lines: list[str],
    runner_result: RunnerResult,
    timer: StepTimer,
    tracker: ProgressTracker | None,
    ctx: Context | None,
    meta: JsonDict,
) -> dict[str, object]:
    """Build `ValidationResult` from a runner result with timing footer + final status line."""
    from aida_mcp.utils.timing import emit_timing_summary

    # Close tracker immediately after steps complete to prevent race condition
    # where progress notifications arrive after the tool result (token invalidated)
    if tracker:
        await tracker.complete("Completed")

    lines: list[str] = []
    for block in header_lines:
        lines.extend(block.splitlines())
    for block in runner_result.lines:
        lines.extend(block.splitlines())
    lines = [truncate_output_line(line, MAX_RESULT_LINE_CHARS) for line in lines]
    lines.append("=" * 60)

    timing_summary = timer.format_summary()
    if timing_summary:
        lines.extend(timing_summary)
        lines.append("")

    await emit_timing_summary(ctx, timer)

    total_duration = timer.total_duration()
    if runner_result.success:
        lines.append(f"✅ VALIDATION PASSED (total: {total_duration:.1f}s)")
        if ctx:
            await ctx.info("✅ Validation passed")
    else:
        lines.append(f"❌ VALIDATION FAILED (total: {total_duration:.1f}s)")
        if ctx:
            await ctx.info("❌ Validation failed")

    full_text = "\n".join(lines)
    truncated_text, truncated, truncated_chars = _truncate_result_text(
        full_text,
        max_chars=MAX_RESULT_CHARS,
    )

    log_paths = [step.log_path for step in runner_result.step_details if step.log_path]
    # Only return tool-level errors when validation fails. On success this is usually noisy
    # (Gradle UP-TO-DATE, background thread logs, etc.). Step details keep per-step logs.
    aggregated_errors: list[str] = []
    aggregated_count = 0
    errors_truncated = False
    if not runner_result.success:
        for step in runner_result.step_details:
            if step.log_path:
                errors, count, _ = extract_error_payload(step.log_path, max_errors=MAX_ERROR_LINES)
                aggregated_errors.extend(errors)
                aggregated_count += count
        if len(aggregated_errors) > MAX_ERROR_LINES:
            aggregated_errors = aggregated_errors[-MAX_ERROR_LINES:]
        errors_truncated = aggregated_count > len(aggregated_errors)

    meta_out: JsonDict = {
        **meta,
        "total_duration_seconds": round(total_duration, 2),
        "log_paths": log_paths,
        "errors": aggregated_errors,
        "error_count": aggregated_count,
        "errors_truncated": errors_truncated,
        "text_truncated": truncated,
        "text_truncated_chars": truncated_chars,
        "text_max_chars": MAX_RESULT_CHARS,
    }
    payload = ValidationResult(
        tool=tool,
        success=runner_result.success,
        text=truncated_text,
        steps=runner_result.step_details,
        meta=meta_out,
    ).to_dict()

    return _ensure_payload_size(payload, max_bytes=MAX_RESULT_BYTES)
