"""멀티모델 봇 통합 프로세스.

3개 봇(잼민이/코덱스/클로디)을 하나의 프로세스에서 실행하며,
공유 DiscussionManager를 통해 봇 간 토론을 지원합니다.
"""

from __future__ import annotations

import asyncio
import json
import logging
from typing import Any, Callable, Coroutine

from bot_utils import replace_thinking_message, send_thinking_message, split_message
from claude_bot import CODE_ANALYSIS_PROMPT_PREFIX
from claude_bot import handle_message as claude_handle
from codex_bot import handle_message as codex_handle
from conversation_memory import BOT_PERSONAS, ConversationMemory
from discussion_manager import DiscussionManager
from engine_v2.bot_api import call_claude, call_codex, call_gemini
from gemini_bot import handle_message as gemini_handle
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message, Update
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters
from telegram.request import HTTPXRequest

from config import CLAUDE_BOT_TOKEN, CODEX_BOT_TOKEN, GEMINI_BOT_TOKEN

logger = logging.getLogger(__name__)

_turn_lock = asyncio.Lock()

# 공유 인스턴스
dm = DiscussionManager()
memory = ConversationMemory(storage_base="/home/jay/workspace/memory/groupchat")

# 봇 username → (Application, engine 호출 함수)
# main() 에서 초기화 후 채워짐
bot_apps: dict[str, tuple[Application, Callable[[str], Coroutine[Any, Any, str]]]] = {}


async def _generate_rolling_summary_safe(chat_id: int) -> None:
    """롤링 서머리 생성 (에러 안전)."""
    try:
        await memory.generate_rolling_summary(chat_id)
    except Exception as exc:
        logger.warning("롤링 서머리 생성 실패 (비치명적): %s", exc)


async def generate_consensus_report(chat_id: int) -> str:
    """전체 대화 내용을 분석하여 합의문 생성."""
    messages = memory.get_context(chat_id, limit=50)
    if not messages:
        return "토론 내용이 없습니다."

    conversation_text = "\n".join(f"{m.sender}: {m.text}" for m in messages)

    # 롤링 서머리가 있으면 포함
    rolling = memory._rolling_summaries.get(chat_id, "")
    rolling_section = f"\n\n[이전 요약]\n{rolling}" if rolling else ""

    prompt = (
        "아래 토론 대화를 분석하여 합의문을 작성해줘.\n"
        "반드시 아래 형식을 따라줘:\n\n"
        "📋 토론 합의문\n\n"
        "[주제]: (토론 주제 한 줄 요약)\n"
        "[참여]: 잼민이(Gemini), 코덱스(GPT), 클로디(Claude)\n\n"
        "✅ 합의사항:\n"
        "1. ...\n"
        "2. ...\n\n"
        "⚠️ 미합의:\n"
        "1. ...\n\n"
        "📌 다음 단계:\n"
        "1. ...\n\n"
        f"{rolling_section}\n\n"
        "<user_content>\n"
        f"{conversation_text}\n"
        "</user_content>"
    )

    try:
        report = await call_claude(prompt, timeout=60)
        return report
    except Exception as exc:
        logger.warning("합의문 생성 실패: %s", exc)
        return f"합의문 생성 실패: {exc}"


async def _generate_consensus_report_safe(chat_id: int) -> None:
    """토론 종료 시 합의문 생성 후 그룹챗에 전송 + 아누 DM."""
    try:
        report = await generate_consensus_report(chat_id)
        # 그룹챗에 합의문 전송 (아무 봇이든 사용)
        for bot_username, (app, _) in bot_apps.items():
            try:
                parts = split_message(report)
                for part in parts:
                    await app.bot.send_message(chat_id=chat_id, text=part)
                break  # 하나의 봇으로만 전송
            except Exception:
                continue

        # 아누에게 DM
        for bot_username, (app, _) in bot_apps.items():
            try:
                await _send_insight_to_owner(app.bot, chat_id, report)
                break
            except Exception:
                continue

        # 롤링 서머리 클리어
        memory._rolling_summaries.pop(chat_id, None)

    except Exception as exc:
        logger.warning("합의문 생성/전송 실패 (비치명적): %s", exc)


async def trigger_next_bot_response(
    next_bot_username: str,
    chat_id: int,
    previous_message: str,
    previous_bot_name: str,
) -> None:
    """다음 턴 봇이 맥락 기반 프롬프트로 응답하도록 트리거합니다."""
    async with _turn_lock:
        await asyncio.sleep(dm.TURN_DELAY)

        # 종료 체크
        if not dm.is_discussion_active(chat_id):
            dm.set_chain_running(chat_id, False)
            return

        if next_bot_username not in bot_apps:
            logger.error("알 수 없는 봇 username: %s", next_bot_username)
            dm.set_chain_running(chat_id, False)
            return

        app, call_fn = bot_apps[next_bot_username]

        try:
            # 생각 중... 메시지 전송
            thinking_msg_id = await send_thinking_message(app.bot, chat_id)

            # 맥락 기반 프롬프트 구성
            context_prompt = memory.format_context(
                chat_id, next_bot_username, phase=dm.get_current_phase(chat_id).value
            )

            # 코덱스 봇이면 deep 모드에 따른 모델 전달
            if next_bot_username == "codex_view_bot":
                codex_model = dm.get_codex_model(chat_id)
                result = await call_codex(context_prompt, model=codex_model)
            elif next_bot_username == "claude_view_bot" and dm.is_code_analysis_mode(chat_id):
                context_prompt = CODE_ANALYSIS_PROMPT_PREFIX + context_prompt
                result = await call_claude(context_prompt, code_analysis=True)
            else:
                result = await call_fn(context_prompt)

            # 생각 중... 메시지를 실제 응답으로 교체
            await replace_thinking_message(app.bot, chat_id, thinking_msg_id, result)

            # 응답을 메모리에 저장
            bot_name = BOT_PERSONAS.get(next_bot_username, {}).get("name", next_bot_username)
            memory.add_message(chat_id, bot_name, result, is_bot=True)

            # 다음 봇 트리거 (on_bot_response가 None이면 자동 종료)
            next_next = dm.on_bot_response(next_bot_username)
            if next_next:
                # 토론 진행 중 → 롤링 서머리 체크 (3라운드마다)
                state = dm._states.get(chat_id)
                if state and state.round_count > 0 and state.round_count % 3 == 0 and state.bot_response_count == 0:
                    asyncio.create_task(_generate_rolling_summary_safe(chat_id))

                asyncio.create_task(
                    trigger_next_bot_response(
                        next_next,
                        chat_id,
                        result,
                        next_bot_username,
                    )
                )
            else:
                # 토론 종료 → 합의문 자동 생성
                dm.set_chain_running(chat_id, False)
                asyncio.create_task(_generate_consensus_report_safe(chat_id))

        except Exception as e:
            logger.error(
                "trigger_next_bot_response 오류 (bot=%s, chat=%d): %s",
                next_bot_username,
                chat_id,
                e,
                exc_info=True,
            )
            dm.set_chain_running(chat_id, False)
            dm.stop_discussion()
            try:
                await app.bot.send_message(
                    chat_id=chat_id,
                    text=f"❌ 응답 중 오류가 발생했습니다: {e}",
                )
            except Exception:
                pass


async def handle_cleanup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """/정리 커맨드 핸들러. 대화 요약(insight)을 생성하여 그룹챗에 전송 + 아누 DM 전송."""
    if update.effective_chat is None or update.effective_message is None:
        return

    chat_id = update.effective_chat.id

    try:
        insight_text = await memory.generate_insight(chat_id)
        # 1. 그룹챗에 응답
        await update.effective_message.reply_text(insight_text)

        # 2. 아누(OWNER) DM으로 insight 전송
        await _send_insight_to_owner(context.bot, chat_id, insight_text)

    except Exception as e:
        logger.error("handle_cleanup_command 오류 (chat=%d): %s", chat_id, e, exc_info=True)
        try:
            await update.effective_message.reply_text(f"❌ 정리 중 오류가 발생했습니다: {e}")
        except Exception:
            pass


async def handle_memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """/메모리 커맨드 핸들러. InlineKeyboard로 요약 목록 표시."""
    if update.effective_chat is None or update.effective_message is None:
        return

    # get_all_summary_files 호출 시도 (없으면 fallback)
    try:
        all_summaries = memory.get_all_summary_files(limit=50)
    except AttributeError:
        all_summaries = memory.get_recent_summaries(limit=5)

    if not all_summaries:
        await update.effective_message.reply_text("📚 저장된 대화 메모리가 없습니다.")
        return

    # 첫 페이지 표시
    await _send_memory_page(update.effective_message, all_summaries, page=0)


async def _send_memory_page(message: Any, summaries: list, page: int = 0, edit: bool = False) -> None:
    """InlineKeyboard 페이지를 표시한다."""
    page_size = 5
    start = page * page_size
    end = start + page_size
    page_items = summaries[start:end]
    total_pages = (len(summaries) + page_size - 1) // page_size

    buttons: list[list[InlineKeyboardButton]] = []
    for s in page_items:
        date = s.get("date", "")
        topic = s.get("topic_tag", "general")
        summary = s.get("summary", "")[:30]
        filename = s.get("filename", "unknown")
        btn_text = f"[{date}] {topic} — {summary}"
        buttons.append([InlineKeyboardButton(btn_text, callback_data=f"mem:{filename}")])

    # 이전/다음 버튼
    nav_buttons: list[InlineKeyboardButton] = []
    if page > 0:
        nav_buttons.append(InlineKeyboardButton("◀ 이전", callback_data=f"mempage:{page - 1}"))
    if page < total_pages - 1:
        nav_buttons.append(InlineKeyboardButton("다음 ▶", callback_data=f"mempage:{page + 1}"))
    if nav_buttons:
        buttons.append(nav_buttons)

    reply_markup = InlineKeyboardMarkup(buttons)
    text = f"📚 대화 메모리 ({page + 1}/{total_pages} 페이지)"

    if edit and hasattr(message, "edit_text"):
        await message.edit_text(text, reply_markup=reply_markup)
    else:
        await message.reply_text(text, reply_markup=reply_markup)


async def handle_memory_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """InlineKeyboard 콜백 처리."""
    query = update.callback_query
    if query is None:
        return
    await query.answer()

    data = query.data or ""

    # query.message를 Message 타입으로 좁힘 (InaccessibleMessage 제외)
    if not isinstance(query.message, Message):
        return
    msg: Message = query.message

    if data.startswith("mempage:"):
        # 페이지 전환
        page = int(data.split(":")[1])
        try:
            all_summaries = memory.get_all_summary_files(limit=50)
        except AttributeError:
            all_summaries = memory.get_recent_summaries(limit=5)
        await _send_memory_page(msg, all_summaries, page=page, edit=True)

    elif data.startswith("mem:"):
        # 요약 상세 보기
        filename = data.split(":", 1)[1]
        summaries_dir = memory._summaries_dir()
        if summaries_dir is None:
            await msg.edit_text("요약 디렉토리를 찾을 수 없습니다.")
            return

        from pathlib import Path  # noqa: PLC0415 – lazy import

        file_path = Path(summaries_dir) / f"{filename}.json"
        if not file_path.exists():
            await msg.edit_text(f"파일을 찾을 수 없습니다: {filename}")
            return

        try:
            summary_data = json.loads(file_path.read_text(encoding="utf-8"))
            lines = [
                f"📄 {summary_data.get('date', '')} — {summary_data.get('topic_tag', '')}",
                "",
                summary_data.get("summary", ""),
                "",
                f"🏷 주제: {', '.join(summary_data.get('key_topics', []))}",
                f"✅ 결정: {', '.join(summary_data.get('key_decisions', []))}",
                f"📋 액션: {', '.join(summary_data.get('action_items', []))}",
                f"🤝 합의: {summary_data.get('consensus_level', '')}",
            ]
            # 뒤로가기 버튼
            back_button = InlineKeyboardMarkup([[InlineKeyboardButton("◀ 목록으로", callback_data="mempage:0")]])
            await msg.edit_text("\n".join(lines), reply_markup=back_button)
        except Exception as exc:
            await msg.edit_text(f"파일 읽기 오류: {exc}")


async def handle_search_memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """/기억 <키워드> 커맨드 핸들러. 요약에서 키워드 검색."""
    if update.effective_chat is None or update.effective_message is None:
        return

    keyword = " ".join(context.args) if context.args else ""
    if not keyword:
        await update.effective_message.reply_text("사용법: /기억 <키워드>")
        return

    # memory의 summaries 디렉토리에서 검색
    summaries_dir = memory._summaries_dir()
    if summaries_dir is None or not summaries_dir.exists():
        await update.effective_message.reply_text("🔍 검색 결과가 없습니다.")
        return

    results = []
    for fp in sorted(summaries_dir.glob("*.json")):
        try:
            data = json.loads(fp.read_text(encoding="utf-8"))
            # key_topics, summary, key_decisions에서 키워드 검색
            searchable = " ".join(
                [
                    data.get("summary", ""),
                    " ".join(data.get("key_topics", [])),
                    " ".join(data.get("key_decisions", [])),
                ]
            )
            if keyword.lower() in searchable.lower():
                results.append(data)
        except Exception:
            continue

    if not results:
        await update.effective_message.reply_text(f"🔍 '{keyword}'에 대한 검색 결과가 없습니다.")
        return

    lines = [f"🔍 '{keyword}' 검색 결과 ({len(results)}건):"]
    for r in results[-5:]:  # 최근 5건만
        date = r.get("date", "")
        topic = r.get("topic_tag", "general")
        summary = r.get("summary", "")[:60]
        lines.append(f"• [{date}] {topic}: {summary}")

    await update.effective_message.reply_text("\n".join(lines))


async def handle_smart_search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """/스마트검색 <질문> 커맨드 핸들러."""
    if update.effective_chat is None or update.effective_message is None:
        return

    query = " ".join(context.args) if context.args else ""
    if not query:
        await update.effective_message.reply_text("사용법: /스마트검색 <질문>")
        return

    chat_id = update.effective_chat.id

    try:
        result = await memory.smart_search(query, chat_id)
        await update.effective_message.reply_text(result)
    except Exception as e:
        logger.error("handle_smart_search_command 오류 (chat=%d): %s", chat_id, e, exc_info=True)
        try:
            await update.effective_message.reply_text(f"스마트검색 중 오류가 발생했습니다: {e}")
        except Exception:
            pass


async def handle_index_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """/목차 커맨드 핸들러. 날짜별 요약 목록 표시."""
    if update.effective_chat is None or update.effective_message is None:
        return

    summaries_dir = memory._summaries_dir()
    if summaries_dir is None or not summaries_dir.exists():
        await update.effective_message.reply_text("📋 저장된 목차가 없습니다.")
        return

    from collections import defaultdict  # noqa: PLC0415 – lazy import

    entries: dict[str, list[str]] = defaultdict(list)

    for fp in sorted(summaries_dir.glob("*.json")):
        try:
            data = json.loads(fp.read_text(encoding="utf-8"))
            date = data.get("date", fp.stem[:10])
            topic = data.get("topic_tag", "general")
            summary = data.get("summary", "")[:40]
            consensus = data.get("consensus_level", "exploratory")
            entry_name = fp.stem.split("_", 1)[1] if "_" in fp.stem else fp.stem
            entries[date].append(f"  - {entry_name}: {summary} ({consensus})")
        except Exception:
            continue

    if not entries:
        await update.effective_message.reply_text("📋 저장된 목차가 없습니다.")
        return

    lines = ["📋 대화 목차"]
    for date in sorted(entries.keys(), reverse=True)[:7]:  # 최근 7일
        lines.append(f"{date}:")
        lines.extend(entries[date])

    await update.effective_message.reply_text("\n".join(lines))


async def _send_insight_to_owner(bot: Any, chat_id: int, insight_text: str) -> None:
    """insight 결과를 OWNER_USER_ID에게 DM으로 전송한다."""
    try:
        from config import OWNER_USER_ID  # noqa: PLC0415 – lazy import (테스트 시 config 없을 수 있음)

        header = f"📋 그룹챗 정리 완료 (chat_id: {chat_id})\n\n"
        # Telegram 메시지 길이 제한 (4096자)
        max_len = 4096 - len(header)
        text = header + (insight_text[:max_len] if len(insight_text) > max_len else insight_text)
        await bot.send_message(chat_id=OWNER_USER_ID, text=text)
    except Exception as exc:
        logger.warning("아누 DM 전송 실패 (비치명적): %s", exc)


async def main() -> None:
    """3개 봇 통합 프로세스 진입점."""
    # 텔레그램 API 타임아웃 확장 (봇 응답이 오래 걸릴 때 httpx ConnectTimeout 방지)
    tg_request = HTTPXRequest(connect_timeout=30, read_timeout=60, write_timeout=60, pool_timeout=30)
    gemini_app = Application.builder().token(GEMINI_BOT_TOKEN).request(tg_request).build()
    codex_app = Application.builder().token(CODEX_BOT_TOKEN).request(tg_request).build()
    claude_app = Application.builder().token(CLAUDE_BOT_TOKEN).request(tg_request).build()

    # bot_apps 전역 딕셔너리 초기화 (trigger_next_bot_response에서 사용)
    bot_apps["gemini_view_bot"] = (gemini_app, call_gemini)
    bot_apps["codex_view_bot"] = (codex_app, call_codex)
    bot_apps["claude_view_bot"] = (claude_app, call_claude)

    # trigger_next_bot_response를 bot_data에 주입하여 각 handle_message에서 접근 가능하도록 함
    for app in [gemini_app, codex_app, claude_app]:
        app.bot_data["trigger_next_bot_response"] = trigger_next_bot_response

    # dm을 바인딩한 래퍼 함수 정의
    async def wrap_gemini(update, context):
        await gemini_handle(update, context, dm=dm, memory=memory)

    async def wrap_codex(update, context):
        await codex_handle(update, context, dm=dm, memory=memory)

    async def wrap_claude(update, context):
        await claude_handle(update, context, dm=dm, memory=memory)

    # 한글 커맨드는 CommandHandler가 ASCII만 지원하므로 MessageHandler + Regex 필터 사용
    korean_commands = [
        (r"^/정리", handle_cleanup_command),
        (r"^/메모리", handle_memory_command),
        (r"^/기억", handle_search_memory_command),
        (r"^/목차", handle_index_command),
        (r"^/스마트검색", handle_smart_search_command),
    ]
    for app in [gemini_app, codex_app, claude_app]:
        for pattern, handler in korean_commands:
            app.add_handler(MessageHandler(filters.Regex(pattern), handler))

    gemini_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, wrap_gemini))
    codex_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, wrap_codex))
    claude_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, wrap_claude))

    # CallbackQueryHandler (InlineKeyboard 콜백)
    for app in [gemini_app, codex_app, claude_app]:
        app.add_handler(CallbackQueryHandler(handle_memory_callback))

    # 3개 봇 동시 polling 시작
    for app in [gemini_app, codex_app, claude_app]:
        await app.initialize()
        await app.start()
        assert app.updater is not None, "Application.updater가 None입니다 (polling 모드 필요)"
        await app.updater.start_polling()

    logger.info("3봇 통합 프로세스 시작 완료")

    try:
        # 무한 대기 (KeyboardInterrupt / SystemExit으로 종료)
        await asyncio.Event().wait()
    except (KeyboardInterrupt, SystemExit):
        pass
    finally:
        for app in [gemini_app, codex_app, claude_app]:
            if app.updater is not None:
                await app.updater.stop()
            await app.stop()
            await app.shutdown()


if __name__ == "__main__":
    logging.basicConfig(
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        level=logging.INFO,
    )
    asyncio.run(main())
