"""Telegram bot core — receives messages, runs Claude, logs everything."""

import asyncio
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path

import httpx
from telegram import Update, Bot
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes

# Session file for conversation persistence
SESSION_FILE = Path.home() / ".claude-link-session.json"

# Queue + lock for sequential Claude CLI execution
_claude_lock = asyncio.Lock()
_message_queue: asyncio.Queue | None = None
_active_proc: asyncio.subprocess.Process | None = None

# Tool icons for console + Telegram status
TOOL_ICONS = {
    "Bash": "⚙️",
    "Read": "📖",
    "Edit": "✏️",
    "Write": "📝",
    "Grep": "🔍",
    "Glob": "📁",
    "WebFetch": "🌐",
    "WebSearch": "🌐",
    "Task": "🤖",
    "Skill": "🎯",
    "TodoWrite": "📋",
}

WELCOME_TEXT = (
    "🌉 claude-link is online!\n\n"
    "Send me a message and I'll forward it to Claude Code.\n\n"
    "Commands:\n"
    "/cancel — Stop current task\n"
    "/new — Start fresh session\n"
    "/status — Bot info\n"
    "/help — All commands"
)

HELP_TEXT = (
    "🌉 claude-link\n\n"
    "Commands:\n"
    "  /cancel — Stop current task + clear queue\n"
    "  /new — Start a fresh conversation\n"
    "  /status — Show bot info\n"
    "  /help — Show this message\n\n"
    "Everything else is sent directly to Claude."
)


def _ts() -> str:
    """Timestamp for console logs."""
    return datetime.now().strftime("%H:%M:%S")


def _log(icon: str, msg: str):
    """Print a timestamped log line."""
    print(f"[{_ts()}] {icon} {msg}")


def _tool_label(name: str, inp: dict) -> str:
    """Short description of a tool call."""
    icon = TOOL_ICONS.get(name, "🔧")
    if name == "Read" and "file_path" in inp:
        return f"{icon} Reading {os.path.basename(inp['file_path'])}"
    if name in ("Edit", "Write") and "file_path" in inp:
        return f"{icon} {'Editing' if name == 'Edit' else 'Writing'} {os.path.basename(inp['file_path'])}"
    if name == "Bash" and "command" in inp:
        return f"{icon} Running: {inp['command'][:60]}"
    if name == "Grep" and "pattern" in inp:
        return f"{icon} Searching: {inp['pattern'][:40]}"
    if name == "Glob" and "pattern" in inp:
        return f"{icon} Finding: {inp['pattern'][:40]}"
    if name == "Task" and "description" in inp:
        return f"{icon} Sub-agent: {inp['description'][:50]}"
    return f"{icon} {name}"


def _load_session() -> str | None:
    if SESSION_FILE.exists():
        data = json.loads(SESSION_FILE.read_text())
        return data.get("session_id")
    return None


def _save_session(session_id: str | None):
    SESSION_FILE.write_text(json.dumps({"session_id": session_id}) + "\n")
    try:
        os.chmod(SESSION_FILE, 0o600)
    except OSError:
        pass


def _clear_session():
    if SESSION_FILE.exists():
        SESSION_FILE.unlink()


def _transcribe_audio(audio_path: str, api_key: str) -> str | None:
    """Transcribe audio using OpenAI Whisper API via httpx."""
    try:
        with open(audio_path, "rb") as f:
            resp = httpx.post(
                "https://api.openai.com/v1/audio/transcriptions",
                headers={"Authorization": f"Bearer {api_key}"},
                data={"model": "whisper-1"},
                files={"file": ("audio.ogg", f, "audio/ogg")},
                timeout=30,
            )
        if resp.status_code == 200:
            return resp.json().get("text", "").strip()
    except Exception as e:
        _log("⚠️", f"Transcription error: {type(e).__name__}")
    return None


async def _run_claude(message: str, cfg: dict, status_msg, session_id: str | None) -> tuple[str | None, str | None]:
    """Run Claude CLI with stream-json, update Telegram status + console logs."""
    global _active_proc
    claude_path = shutil.which(cfg.get("claude_path", "claude")) or cfg.get("claude_path", "claude")
    workspace = cfg.get("workspace", str(Path.home()))

    args = ["-p", message, "--output-format", "stream-json", "--verbose"]
    if session_id:
        args.extend(["--resume", session_id])

    env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}

    # Use exec on Unix (safer, no shell injection risk), shell only on Windows
    if sys.platform == "win32":
        cmd_line = subprocess.list2cmdline([claude_path] + args)
        proc = await asyncio.create_subprocess_shell(
            cmd_line,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=workspace,
            env=env,
        )
    else:
        proc = await asyncio.create_subprocess_exec(
            claude_path, *args,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=workspace,
            env=env,
        )

    _active_proc = proc

    status_lines = ["⏳ Working..."]
    last_edit = 0
    result_text = None
    new_session_id = None
    expired = False
    start = time.time()

    async def _update_status():
        nonlocal last_edit
        now = time.time()
        if now - last_edit < 2:
            return
        last_edit = now
        try:
            await status_msg.edit_text("\n".join(status_lines[-10:]))
        except Exception:
            pass

    async for raw in proc.stdout:
        line = raw.decode("utf-8", errors="replace").strip()
        if not line:
            continue
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            if "no conversation found" in line.lower() or "session" in line.lower():
                expired = True
            continue

        etype = event.get("type")

        if etype == "assistant":
            for block in event.get("message", {}).get("content", []):
                if block.get("type") == "tool_use":
                    label = _tool_label(block.get("name", ""), block.get("input", {}))
                    status_lines.append(label)
                    _log("  ", label)
                    await _update_status()

        elif etype == "result":
            result_text = event.get("result")
            new_session_id = event.get("session_id")

    await proc.wait()
    _active_proc = None

    elapsed = int(time.time() - start)
    status_lines.append(f"✅ Done ({elapsed}s)")
    try:
        await status_msg.edit_text("\n".join(status_lines[-10:]))
    except Exception:
        pass

    if expired:
        return None, None

    if result_text is None:
        stderr = ""
        if proc.stderr:
            stderr = (await proc.stderr.read()).decode("utf-8", errors="replace")
        if "no conversation found" in stderr.lower():
            return None, None
        return stderr or "No response from Claude.", None

    return result_text, new_session_id


async def _queue_worker():
    """Process queued messages one at a time."""
    while True:
        text, update, cfg = await _message_queue.get()
        try:
            async with _claude_lock:
                await _send_to_claude_inner(text, update, cfg)
        except Exception as e:
            _log("⚠️", f"Queue worker error: {type(e).__name__}")
        finally:
            _message_queue.task_done()


async def _send_to_claude(text: str, update: Update, cfg: dict):
    """Enqueue a message for Claude. If busy, notifies the user it's queued."""
    if _claude_lock.locked():
        pos = _message_queue.qsize() + 1
        await update.message.reply_text(f"📋 Queued (position {pos}). Will process when current task finishes.")
        _log("📋", f"Message queued (position {pos})")

    await _message_queue.put((text, update, cfg))


async def _send_to_claude_inner(text: str, update: Update, cfg: dict):
    """Inner logic: send text to Claude and reply (called under lock)."""
    status_msg = await update.message.reply_text("⏳ Working...")
    _log("🤖", "Claude working...")

    session_id = _load_session()

    # Try with existing session
    if session_id:
        response, new_sid = await _run_claude(text, cfg, status_msg, session_id)
        if response is None:
            _log("🔄", "Session expired, starting fresh")
            _clear_session()
            session_id = None

    # Fresh session
    if not session_id:
        prompt = f"First, silently read CLAUDE.md for context. Then respond to: {text}"
        response, new_sid = await _run_claude(prompt, cfg, status_msg, None)

    if new_sid:
        _save_session(new_sid)

    if not response or not response.strip():
        response = "(No response from Claude)"

    # Truncate if too long for Telegram
    if len(response) > 4000:
        response = response[:4000] + "\n\n... (truncated)"

    _log("📤", f"Reply: \"{response[:80]}{'...' if len(response) > 80 else ''}\"")
    await update.message.reply_text(response)


def _check_auth(update: Update, cfg: dict) -> bool:
    """Check if the message is from the authorized chat. Logs unauthorized attempts."""
    if update.effective_chat.id != cfg["chat_id"]:
        user = update.effective_user
        name = f"{user.first_name or ''} (id={user.id})" if user else f"id={update.effective_chat.id}"
        _log("⛔", f"Unauthorized access attempt from {name}")
        return False
    return True


async def _handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle incoming text messages."""
    cfg = context.bot_data["cfg"]

    if not _check_auth(update, cfg):
        return

    text = update.message.text
    user = update.effective_user.first_name or "User"

    _log("📩", f"Message from {user}: \"{text[:80]}{'...' if len(text) > 80 else ''}\"")

    await _send_to_claude(text, update, cfg)


async def _handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle voice messages — transcribe and send to Claude."""
    cfg = context.bot_data["cfg"]

    if not _check_auth(update, cfg):
        return

    api_key = cfg.get("openai_api_key")
    if not api_key:
        await update.message.reply_text(
            "🎤 Voice messages are not enabled.\n\n"
            "To enable, run in your terminal:\n"
            "  claude-bridge --enable-voice"
        )
        return

    user = update.effective_user.first_name or "User"
    _log("🎤", f"Voice message from {user}")

    await update.message.reply_text("🎤 Transcribing...")

    voice = update.message.voice
    file = await context.bot.get_file(voice.file_id)

    with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
        tmp_path = tmp.name

    await file.download_to_drive(tmp_path)

    try:
        transcript = _transcribe_audio(tmp_path, api_key)

        if not transcript:
            await update.message.reply_text("❌ Could not transcribe voice message.")
            return

        _log("📝", f"Transcribed: \"{transcript[:80]}{'...' if len(transcript) > 80 else ''}\"")
        await update.message.reply_text(f"📝 {transcript}")

        voice_text = f"[This is a voice message transcription]: {transcript}"
        await _send_to_claude(voice_text, update, cfg)

    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


def _drain_queue() -> int:
    """Empty the message queue. Returns number of messages cleared."""
    count = 0
    while not _message_queue.empty():
        try:
            _message_queue.get_nowait()
            _message_queue.task_done()
            count += 1
        except asyncio.QueueEmpty:
            break
    return count


async def _handle_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /cancel command — kill active Claude process and clear queue."""
    cfg = context.bot_data["cfg"]
    if not _check_auth(update, cfg):
        return

    global _active_proc

    if _active_proc is None or _active_proc.returncode is not None:
        await update.message.reply_text("Nothing running.")
        return

    # Kill the process
    try:
        _active_proc.terminate()
        try:
            await asyncio.wait_for(_active_proc.wait(), timeout=2)
        except asyncio.TimeoutError:
            _active_proc.kill()
    except ProcessLookupError:
        pass

    _active_proc = None

    # Drain the queue
    cleared = _drain_queue()

    suffix = f" ({cleared} queued message{'s' if cleared != 1 else ''} cleared)" if cleared else ""
    _log("⛔", f"Cancelled by user{suffix}")
    await update.message.reply_text(f"⛔ Cancelled{suffix}")


async def _handle_new(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /new command — clear session."""
    cfg = context.bot_data["cfg"]
    if not _check_auth(update, cfg):
        return

    _clear_session()
    _log("🔄", "Session cleared by user")
    await update.message.reply_text("Session cleared. Next message starts fresh.")


async def _handle_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /status command."""
    cfg = context.bot_data["cfg"]
    if not _check_auth(update, cfg):
        return

    has_session = _load_session() is not None
    voice = "Enabled" if cfg.get("openai_api_key") else "Disabled"
    await update.message.reply_text(
        f"🌉 claude-link\n"
        f"Session: {'Active' if has_session else 'None'}\n"
        f"Voice: {voice}\n"
        f"Workspace: {cfg['workspace']}\n"
        f"Claude: {cfg.get('claude_path', 'claude')}"
    )


async def _handle_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /help command."""
    cfg = context.bot_data["cfg"]
    if not _check_auth(update, cfg):
        return

    await update.message.reply_text(HELP_TEXT)


async def _send_welcome(app: Application):
    """Send a welcome message and start the queue worker."""
    global _message_queue
    _message_queue = asyncio.Queue()
    asyncio.create_task(_queue_worker())

    cfg = app.bot_data["cfg"]
    try:
        await app.bot.send_message(chat_id=cfg["chat_id"], text=WELCOME_TEXT)
        _log("📤", "Welcome message sent")
    except Exception as e:
        _log("⚠️", f"Could not send welcome message: {e}")


def run_bot(cfg: dict):
    """Start the Telegram bot. Blocks until Ctrl+C."""
    _log("🚀", "Bot starting...")

    app = Application.builder().token(cfg["telegram_token"]).build()
    app.bot_data["cfg"] = cfg

    app.add_handler(CommandHandler("cancel", _handle_cancel))
    app.add_handler(CommandHandler("new", _handle_new))
    app.add_handler(CommandHandler("status", _handle_status))
    app.add_handler(CommandHandler("help", _handle_help))
    app.add_handler(MessageHandler(filters.VOICE, _handle_voice))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, _handle_message))

    # Send welcome message on startup
    app.post_init = _send_welcome

    _log("✅", f"Listening for messages from chat {cfg['chat_id']}")
    print(f"[{_ts()}]    Press Ctrl+C to stop\n")

    app.run_polling(allowed_updates=Update.ALL_TYPES)
