#!/usr/bin/env python3
"""
Batch-тестирование запросов из файла examples.txt.

Использование:
    # Прогнать все примеры
    python tools/test_batch.py

    # Прогнать конкретные строки (диапазон)
    python tools/test_batch.py --lines 1-10

    # Прогнать одну строку
    python tools/test_batch.py --lines 5

    # Свой файл с примерами
    python tools/test_batch.py --file my_examples.txt

    # Только ошибки / пустые результаты
    python tools/test_batch.py --errors-only

    # Сохранить результаты в JSON
    python tools/test_batch.py --output results.json

    # Задержка между запросами (сек)
    python tools/test_batch.py --delay 2
"""

import asyncio
import argparse
import json
import os
import sys
import uuid
import time
from datetime import datetime
from pathlib import Path
from typing import Optional

import httpx

sys.path.insert(0, str(Path(__file__).parent.parent))

try:
    from dotenv import load_dotenv
except Exception:
    load_dotenv = None

if load_dotenv:
    load_dotenv()


# ── Colors ──────────────────────────────────────────────────────────────────

class C:
    _use_color = sys.stdout.isatty()

    RESET = "\033[0m" if _use_color else ""
    BOLD = "\033[1m" if _use_color else ""
    DIM = "\033[2m" if _use_color else ""
    RED = "\033[31m" if _use_color else ""
    GREEN = "\033[32m" if _use_color else ""
    YELLOW = "\033[33m" if _use_color else ""
    BLUE = "\033[34m" if _use_color else ""
    MAGENTA = "\033[35m" if _use_color else ""
    CYAN = "\033[36m" if _use_color else ""
    WHITE = "\033[37m" if _use_color else ""
    BG_RED = "\033[41m" if _use_color else ""
    BG_GREEN = "\033[42m" if _use_color else ""


def _default_base_url() -> str:
    api_base = os.getenv("API_BASE_URL")
    if api_base:
        return api_base
    host = os.getenv("API_HOST", "localhost")
    if host in ("0.0.0.0", "::"):
        host = "localhost"
    port = os.getenv("API_PORT", "8000")
    return f"http://{host}:{port}"


def load_examples(file_path: Path, line_range: Optional[str] = None) -> list[tuple[int, str]]:
    """Загрузить примеры из файла. Возвращает [(line_num, text), ...]."""
    with open(file_path, "r", encoding="utf-8") as f:
        lines = [line.strip() for line in f.readlines()]

    # Нумерация с 1
    all_examples = [(i + 1, line) for i, line in enumerate(lines) if line]

    if not line_range:
        return all_examples

    # Парсим диапазон: "5" или "1-10"
    if "-" in line_range:
        start, end = line_range.split("-", 1)
        start, end = int(start), int(end)
    else:
        start = end = int(line_range)

    return [(n, t) for n, t in all_examples if start <= n <= end]


async def send_request(client: httpx.AsyncClient, base_url: str, text: str,
                       session_id: str, thread_id: str) -> dict:
    """Отправить запрос в бэкенд."""
    request = {
        "session_id": session_id,
        "thread_id": thread_id,
        "mode": "chat",
        "input": {
            "type": "text",
            "text": text,
            "audio_b64": None,
            "audio_format": None,
            "sample_rate_hz": 16000,
            "language": "ru-RU"
        },
        "client": {
            "device": "web",
            "timezone": "Europe/Prague",
            "voice_enabled": False,
            "tts_enabled": False,
            "speech_provider_preference": "azure"
        }
    }

    response = await client.post(f"{base_url}/v1/chat/turn", json=request)
    response.raise_for_status()
    return response.json()


def extract_info(response: dict) -> dict:
    """Извлечь ключевую информацию из ответа."""
    ui = response.get("ui", {})
    dashboards = ui.get("dashboards", [])
    dashboard_id = ui.get("dashboard_id", "")
    assistant_text = response.get("assistant_text", "")
    analytic_text = response.get("analytic_text", "")
    clarification = response.get("clarification", {})
    error = response.get("error_details")

    # Собираем виджеты и кол-во данных
    widgets = []
    total_data_points = 0
    for group in dashboards:
        for card in group.get("cards", []):
            widget_id = card.get("widget_id", "")
            payload = card.get("payload", {})
            data = payload.get("data", [])
            data_count = len(data) if isinstance(data, list) else 0
            total_data_points += data_count
            widgets.append({
                "widget_id": widget_id,
                "type": card.get("thumbnail_spec", {}).get("widgetType", ""),
                "data_rows": data_count,
            })

    # Ответ считается текстовым (quick_query с analytic_as_assistant и т.п.),
    # если нет видимых данных, но AI дал содержательный ответ.
    # analytic_text может быть пуст (перенесён в assistant_text через analytic_as_assistant),
    # поэтому проверяем оба поля, но исключаем фразы-отказы.
    _fail_markers = [
        "не смог определить",
        "не удалось",
        "не удалось выполнить",
        "не удалось получить",
        "не удалось сгенерировать",
        "попробуйте позже",
        "попробуйте переформулировать",
    ]
    text_content = analytic_text.strip() or assistant_text.strip()
    is_failure_text = any(m in text_content.lower() for m in _fail_markers)
    has_text_response = (
        bool(text_content)
        and total_data_points == 0
        and not is_failure_text
    )

    return {
        "dashboard_id": dashboard_id,
        "assistant_text": assistant_text,
        "analytic_text": analytic_text,
        "widgets": widgets,
        "widget_count": len(widgets),
        "total_data_points": total_data_points,
        "has_text_response": has_text_response,
        "has_clarification": clarification.get("needed", False),
        "clarification_text": clarification.get("question", ""),
        "has_error": error is not None,
        "error": error,
    }


def get_status_label(info: dict) -> str:
    """Вернуть текстовый статус без цветов."""
    if info["has_error"]:
        return "ERR"
    elif info["has_clarification"]:
        return "?"
    elif info.get("has_text_response", False):
        return "TXT"
    elif info["total_data_points"] > 0:
        return "OK"
    else:
        return "0d"


def print_result(line_num: int, text: str, info: dict, elapsed: float,
                 errors_only: bool, show_widgets: bool = False):
    """Вывести результат одного запроса."""
    # Определяем статус
    has_data = info["total_data_points"] > 0
    has_error = info["has_error"]
    has_clarification = info["has_clarification"]
    has_text_response = info.get("has_text_response", False)

    if has_error:
        status = f"{C.BG_RED}{C.WHITE} ERR {C.RESET}"
    elif has_clarification:
        status = f"{C.YELLOW}  ?  {C.RESET}"
    elif has_text_response:
        status = f"{C.GREEN} TXT {C.RESET}"
    elif not has_data:
        status = f"{C.RED} 0d  {C.RESET}"
    else:
        status = f"{C.GREEN} OK  {C.RESET}"

    is_ok = has_data or has_text_response
    if errors_only and is_ok and not has_error and not has_clarification:
        return

    # Шапка
    print(f"\n{C.DIM}{'─' * 90}{C.RESET}")
    print(f" {status} {C.BOLD}#{line_num:3d}{C.RESET}  {C.CYAN}{text}{C.RESET}")
    print(f"       {C.DIM}dashboard={C.RESET}{info['dashboard_id']}"
          f"  {C.DIM}widgets={C.RESET}{info['widget_count']}"
          f"  {C.DIM}data={C.RESET}{info['total_data_points']}"
          f"  {C.DIM}time={C.RESET}{elapsed:.1f}s")

    # Виджеты
    if show_widgets and info["widgets"]:
        for w in info["widgets"]:
            data_str = f"{w['data_rows']} rows" if w['data_rows'] > 0 else f"{C.RED}0 rows{C.RESET}"
            print(f"       {C.DIM}├─{C.RESET} {w['widget_id']}"
                  f"  {C.DIM}({w['type']}){C.RESET}"
                  f"  → {data_str}")

    # Текст ассистента (первые 120 символов)
    if info["assistant_text"]:
        txt = info["assistant_text"][:120]
        if len(info["assistant_text"]) > 120:
            txt += "..."
        print(f"       {C.DIM}💬{C.RESET} {txt}")

    # Аналитический текст
    if info.get("analytic_text"):
        atxt = info["analytic_text"][:200]
        if len(info["analytic_text"]) > 200:
            atxt += "..."
        print(f"       {C.MAGENTA}📊 {atxt}{C.RESET}")

    # Уточнение
    if info["has_clarification"]:
        print(f"       {C.YELLOW}❓ {info['clarification_text']}{C.RESET}")

    # Ошибка
    if info["has_error"]:
        err = info["error"]
        print(f"       {C.RED}❌ {err.get('message', str(err))}{C.RESET}")


def _md_escape(text: str, max_len: int = 0) -> str:
    """Экранировать текст для ячейки markdown-таблицы."""
    if not text:
        return ""
    # Убираем переносы строк и pipe
    t = text.replace("\n", " ").replace("\r", "").replace("|", "\\|")
    if max_len and len(t) > max_len:
        t = t[:max_len] + "…"
    return t


def md_write_header(f):
    """Записать заголовок таблицы."""
    f.write("| # | Status | Вопрос | Ответ | Подробнее | dashboard | widgets | data | time |\n")
    f.write("|--:|--------|--------|-------|-----------|-----------|--------:|-----:|-----:|\n")


def md_write_result(f, info: dict, show_widgets: bool = False):
    """Записать строку таблицы."""
    status = get_status_label(info)
    emoji = {"OK": "✅", "TXT": "✅", "ERR": "❌", "0d": "⚠️", "?": "❓"}.get(status, "")
    line_num = info["line_num"]
    elapsed = info.get("elapsed", 0)

    question = _md_escape(info["text"], 60)
    asst = _md_escape(info["assistant_text"], 120)
    anlt = _md_escape(info.get("analytic_text", ""), 120)

    # Для ошибок/уточнений — показать в assistant_text
    if info["has_error"] and not asst:
        err = info["error"]
        asst = _md_escape(err.get("message", str(err)), 120)
    if info["has_clarification"] and not asst:
        asst = _md_escape(info["clarification_text"], 120)

    dash = info["dashboard_id"] or ""

    f.write(
        f"| {line_num} "
        f"| {emoji} {status} "
        f"| {question} "
        f"| {asst} "
        f"| {anlt} "
        f"| {dash} "
        f"| {info['widget_count']} "
        f"| {info['total_data_points']} "
        f"| {elapsed:.1f}s |\n"
    )


def md_write_summary(f, results: list[dict]):
    """Записать итоги после таблицы."""
    total = len(results)
    ok = sum(1 for r in results if r["total_data_points"] > 0 and not r["has_error"])
    txt = sum(1 for r in results if r.get("has_text_response") and not r["has_error"])
    empty = sum(1 for r in results if r["total_data_points"] == 0 and not r.get("has_text_response")
                and not r["has_error"] and not r["has_clarification"])
    errors = sum(1 for r in results if r["has_error"])
    clarifications = sum(1 for r in results if r["has_clarification"])
    total_time = sum(r.get("elapsed", 0) for r in results)

    f.write(f"\n## Итого\n\n")
    f.write(f"**{total}** запросов за **{total_time:.1f}s** (avg {total_time / max(total, 1):.1f}s)\n\n")
    f.write(f"| Status | Кол-во |\n")
    f.write(f"|--------|-------:|\n")
    f.write(f"| ✅ OK | {ok} |\n")
    f.write(f"| ✅ TXT | {txt} |\n")
    f.write(f"| ⚠️ Empty | {empty} |\n")
    f.write(f"| ❌ Error | {errors} |\n")
    f.write(f"| ❓ Clarif | {clarifications} |\n")


def print_summary(results: list[dict]):
    """Итоговая таблица."""
    total = len(results)
    ok = sum(1 for r in results if r["total_data_points"] > 0 and not r["has_error"])
    txt = sum(1 for r in results if r.get("has_text_response") and not r["has_error"])
    empty = sum(1 for r in results if r["total_data_points"] == 0 and not r.get("has_text_response")
                and not r["has_error"] and not r["has_clarification"])
    errors = sum(1 for r in results if r["has_error"])
    clarifications = sum(1 for r in results if r["has_clarification"])
    total_time = sum(r.get("elapsed", 0) for r in results)

    print(f"\n{C.BOLD}{'═' * 90}{C.RESET}")
    print(f" {C.BOLD}ИТОГО:{C.RESET} {total} запросов за {total_time:.1f}s"
          f"  (avg {total_time / max(total, 1):.1f}s)")
    print(f"   {C.GREEN}✓ OK:     {ok}{C.RESET}")
    print(f"   {C.GREEN}✓ TXT:    {txt}{C.RESET}")
    print(f"   {C.RED}✗ Empty:  {empty}{C.RESET}")
    print(f"   {C.RED}✗ Error:  {errors}{C.RESET}")
    print(f"   {C.YELLOW}? Clarif: {clarifications}{C.RESET}")
    print(f"{C.BOLD}{'═' * 90}{C.RESET}")

    # Проблемные запросы
    problems = [r for r in results
                if (r["total_data_points"] == 0 and not r.get("has_text_response")) or r["has_error"]]
    if problems:
        print(f"\n{C.BOLD}{C.RED}Проблемные запросы:{C.RESET}")
        for r in problems:
            reason = "ERROR" if r["has_error"] else "0 data"
            print(f"  #{r['line_num']:3d}  [{reason}]  {r['text']}")


async def main():
    parser = argparse.ArgumentParser(description="Batch-тестирование запросов")
    parser.add_argument("--file", type=Path, default=Path("examples.txt"),
                        help="Файл с примерами (default: examples.txt)")
    parser.add_argument("--lines", type=str, default=None,
                        help="Диапазон строк: '5' или '1-10'")
    parser.add_argument("--url", default=_default_base_url(),
                        help="URL бэкенда")
    parser.add_argument("--delay", type=float, default=0.5,
                        help="Задержка между запросами (сек)")
    parser.add_argument("--errors-only", action="store_true",
                        help="Показывать только ошибки и пустые результаты")
    parser.add_argument("--output", type=Path, default=None,
                        help="Сохранить результаты в JSON")
    parser.add_argument("--timeout", type=float, default=120.0,
                        help="Таймаут запроса (сек)")
    parser.add_argument("--widgets", action="store_true",
                        help="Показывать список виджетов для каждого запроса")
    parser.add_argument("--md", type=Path, default=None,
                        help="Сохранить результаты в Markdown-файл")

    args = parser.parse_args()

    # Загружаем примеры
    examples_file = args.file
    if not examples_file.is_absolute():
        examples_file = Path(__file__).parent.parent / examples_file

    if not examples_file.exists():
        print(f"{C.RED}Файл не найден: {examples_file}{C.RESET}")
        sys.exit(1)

    examples = load_examples(examples_file, args.lines)
    if not examples:
        print(f"{C.RED}Нет примеров для тестирования{C.RESET}")
        sys.exit(1)

    print(f"{C.BOLD}Batch Test{C.RESET}")
    print(f"  URL:      {args.url}")
    print(f"  File:     {examples_file}")
    print(f"  Examples: {len(examples)}")
    print(f"  Delay:    {args.delay}s")

    # Открываем md-файл если указан
    md_file = None
    if args.md:
        md_path = args.md if args.md.is_absolute() else Path(__file__).parent.parent / args.md
        md_file = open(md_path, "w", encoding="utf-8")
        md_file.write(f"# Batch Test Results\n\n")
        md_file.write(f"**{examples_file.name}** · {len(examples)} examples · {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n")
        md_write_header(md_file)

    results = []

    async with httpx.AsyncClient(timeout=args.timeout) as client:
        for i, (line_num, text) in enumerate(examples):
            # Новая сессия на каждый запрос, чтобы исключить влияние контекста
            session_id = str(uuid.uuid4())
            thread_id = str(uuid.uuid4())
            t0 = time.time()

            try:
                response = await send_request(client, args.url, text, session_id, thread_id)
                elapsed = time.time() - t0
                info = extract_info(response)
                info["elapsed"] = elapsed
                info["line_num"] = line_num
                info["text"] = text
                info["raw_response"] = response

                print_result(line_num, text, info, elapsed, args.errors_only, args.widgets)
                results.append(info)

            except httpx.HTTPStatusError as e:
                elapsed = time.time() - t0
                error_body = ""
                try:
                    error_body = e.response.text[:200]
                except Exception:
                    pass
                info = {
                    "line_num": line_num,
                    "text": text,
                    "dashboard_id": "",
                    "assistant_text": "",
                    "analytic_text": "",
                    "widgets": [],
                    "widget_count": 0,
                    "total_data_points": 0,
                    "has_text_response": False,
                    "has_clarification": False,
                    "clarification_text": "",
                    "has_error": True,
                    "error": {"message": f"HTTP {e.response.status_code}: {error_body}"},
                    "elapsed": elapsed,
                }
                print_result(line_num, text, info, elapsed, args.errors_only, args.widgets)
                results.append(info)

            except Exception as e:
                elapsed = time.time() - t0
                info = {
                    "line_num": line_num,
                    "text": text,
                    "dashboard_id": "",
                    "assistant_text": "",
                    "analytic_text": "",
                    "widgets": [],
                    "widget_count": 0,
                    "total_data_points": 0,
                    "has_text_response": False,
                    "has_clarification": False,
                    "clarification_text": "",
                    "has_error": True,
                    "error": {"message": str(e)},
                    "elapsed": elapsed,
                }
                print_result(line_num, text, info, elapsed, args.errors_only, args.widgets)
                results.append(info)

            # Пишем в md сразу после каждого запроса
            if md_file:
                md_write_result(md_file, info)
                md_file.flush()

            # Задержка между запросами
            if i < len(examples) - 1:
                await asyncio.sleep(args.delay)

    # Итого
    print_summary(results)

    # Записываем summary в md
    if md_file:
        md_write_summary(md_file, results)
        md_file.close()
        print(f"\n{C.GREEN}Markdown сохранён: {args.md}{C.RESET}")

    # Сохранение в JSON
    if args.output:
        # Убираем raw_response для компактности
        save_data = []
        for r in results:
            entry = {k: v for k, v in r.items() if k != "raw_response"}
            save_data.append(entry)
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(save_data, f, indent=2, ensure_ascii=False)
        print(f"\n{C.GREEN}Результаты сохранены: {args.output}{C.RESET}")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print(f"\n{C.YELLOW}Interrupted{C.RESET}")
