"""
bot.py - Discord bot client with session management and command routing

Features:
- Login flow with username/password
- Session management with JWT tokens
- AI-powered command parsing via registry
- Background task loop for polling
"""

import discord
from discord.ext import tasks
import os
import sys
import json
import base64
import requests
import bcrypt
import pickle

from bot.command_registry import get_handler, list_registered
import ai.parser as ai_parser
import bot.commands.routines  # noqa: F401 - registers handler
import bot.commands.medications  # noqa: F401 - registers handler
import bot.commands.knowledge  # noqa: F401 - registers handler

DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
API_URL = os.getenv("API_URL", "http://app:5000")

user_sessions = {}
login_state = {}
message_history = {}
user_cache = {}
CACHE_FILE = "/app/user_cache.pkl"

intents = discord.Intents.default()
intents.message_content = True

client = discord.Client(intents=intents)


def decodeJwtPayload(token):
    payload = token.split(".")[1]
    payload += "=" * (4 - len(payload) % 4)
    return json.loads(base64.urlsafe_b64decode(payload))


def apiRequest(method, endpoint, token=None, data=None):
    url = f"{API_URL}{endpoint}"
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    try:
        resp = getattr(requests, method)(url, headers=headers, json=data, timeout=10)
        try:
            return resp.json(), resp.status_code
        except ValueError:
            return {}, resp.status_code
    except requests.RequestException:
        return {"error": "API unavailable"}, 503


def loadCache():
    try:
        if os.path.exists(CACHE_FILE):
            with open(CACHE_FILE, "rb") as f:
                global user_cache
                user_cache = pickle.load(f)
                print(f"Loaded cache for {len(user_cache)} users")
    except Exception as e:
        print(f"Error loading cache: {e}")


def saveCache():
    try:
        with open(CACHE_FILE, "wb") as f:
            pickle.dump(user_cache, f)
    except Exception as e:
        print(f"Error saving cache: {e}")


def hashPassword(password):
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")


def verifyPassword(password, hashed):
    return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))


def getCachedUser(discord_id):
    return user_cache.get(discord_id)


def setCachedUser(discord_id, user_data):
    user_cache[discord_id] = user_data
    saveCache()


def negotiateToken(discord_id, username, password):
    cached = getCachedUser(discord_id)
    if (
        cached
        and cached.get("username") == username
        and verifyPassword(password, cached.get("hashed_password"))
    ):
        result, status = apiRequest(
            "post", "/api/login", data={"username": username, "password": password}
        )
        if status == 200 and "token" in result:
            token = result["token"]
            payload = decodeJwtPayload(token)
            user_uuid = payload["sub"]
            setCachedUser(
                discord_id,
                {
                    "hashed_password": cached["hashed_password"],
                    "user_uuid": user_uuid,
                    "username": username,
                },
            )
            return token, user_uuid
        return None, None

    result, status = apiRequest(
        "post", "/api/login", data={"username": username, "password": password}
    )
    if status == 200 and "token" in result:
        token = result["token"]
        payload = decodeJwtPayload(token)
        user_uuid = payload["sub"]
        setCachedUser(
            discord_id,
            {
                "hashed_password": hashPassword(password),
                "user_uuid": user_uuid,
                "username": username,
            },
        )
        return token, user_uuid
    return None, None


async def handleAuthFailure(message):
    discord_id = message.author.id
    user_sessions.pop(discord_id, None)
    await message.channel.send(
        "Your session has expired. Send any message to log in again."
    )


async def handleLoginStep(message):
    discord_id = message.author.id
    state = login_state[discord_id]

    if state["step"] == "username":
        state["username"] = message.content.strip()
        state["step"] = "password"
        await message.channel.send("Password?")

    elif state["step"] == "password":
        username = state["username"]
        password = message.content.strip()
        del login_state[discord_id]

        token, user_uuid = negotiateToken(discord_id, username, password)

        if token and user_uuid:
            user_sessions[discord_id] = {
                "token": token,
                "user_uuid": user_uuid,
                "username": username,
            }
            registered = ", ".join(list_registered()) or "none"
            await message.channel.send(
                f"Welcome back **{username}**!\n\n"
                f"Registered modules: {registered}\n\n"
                f"Send 'help' for available commands."
            )
        else:
            await message.channel.send(
                "Invalid credentials. Send any message to try again."
            )


async def sendHelpMessage(message):
    help_msg = """**🤖 Synculous Bot - Natural Language Commands**

Just talk to me naturally! Here are some examples:

**💊 Medications:**
• "add lsd 50 mcg every tuesday at 4:20pm"
• "take my wellbutrin"
• "what meds do i have today?"
• "show my refills"
• "snooze my reminder for 30 minutes"
• "check adherence"

**📋 Routines:**
• "create morning routine with brush teeth, shower, eat"
• "start my morning routine"
• "done" (complete current step)
• "skip" (skip current step)
• "pause/resume" (pause or continue)
• "what steps are in my routine?"
• "schedule workout for monday wednesday friday at 7am"
• "show my stats"

**💡 Tips:**
• I understand natural language, typos, and slang
• If I'm unsure, I'll ask for clarification
• For important actions, I'll ask you to confirm with "yes" or "no"
• When you're in a routine, shortcuts like "done", "skip", "pause" work automatically"""
    await message.channel.send(help_msg)


async def checkActiveSession(session):
    """Check if user has an active routine session and return details."""
    token = session.get("token")
    if not token:
        return None

    resp, status = apiRequest("get", "/api/sessions/active", token)
    if status == 200 and "session" in resp:
        return resp
    return None


async def handleConfirmation(message, session):
    """Handle yes/no confirmation responses. Returns True if handled."""
    discord_id = message.author.id
    user_input = message.content.lower().strip()

    if "pending_confirmations" not in session:
        return False

    # Check for any pending confirmations
    pending = session["pending_confirmations"]
    if not pending:
        return False

    # Get the most recent pending confirmation
    confirmation_id = list(pending.keys())[-1]
    confirmation_data = pending[confirmation_id]

    if user_input in ("yes", "y", "yeah", "sure", "ok", "confirm"):
        # Execute the confirmed action
        del pending[confirmation_id]

        interaction_type = confirmation_data.get("interaction_type")
        handler = get_handler(interaction_type)

        if handler:
            # Create a fake parsed object for the handler
            fake_parsed = confirmation_data.copy()
            fake_parsed["needs_confirmation"] = False
            await handler(message, session, fake_parsed)
        return True

    elif user_input in ("no", "n", "nah", "cancel", "abort"):
        del pending[confirmation_id]
        await message.channel.send("❌ Cancelled.")
        return True

    return False


async def handleActiveSessionShortcuts(message, session, active_session):
    """Handle shortcuts like 'done', 'skip', 'next' when in active session."""
    user_input = message.content.lower().strip()

    # Map common shortcuts to actions
    shortcuts = {
        "done": ("routine", "complete"),
        "finished": ("routine", "complete"),
        "complete": ("routine", "complete"),
        "next": ("routine", "complete"),
        "skip": ("routine", "skip"),
        "pass": ("routine", "skip"),
        "pause": ("routine", "pause"),
        "hold": ("routine", "pause"),
        "resume": ("routine", "resume"),
        "continue": ("routine", "resume"),
        "stop": ("routine", "cancel"),
        "quit": ("routine", "cancel"),
        "abort": ("routine", "abort"),
    }

    if user_input in shortcuts:
        interaction_type, action = shortcuts[user_input]
        handler = get_handler(interaction_type)
        if handler:
            fake_parsed = {"action": action}
            await handler(message, session, fake_parsed)
            return True

    return False


async def routeCommand(message):
    discord_id = message.author.id
    session = user_sessions[discord_id]
    user_input = message.content.lower()

    if "help" in user_input or "what can i say" in user_input:
        await sendHelpMessage(message)
        return

    # Check for active session first
    active_session = await checkActiveSession(session)

    # Handle confirmation responses
    confirmation_handled = await handleConfirmation(message, session)
    if confirmation_handled:
        return

    # Handle shortcuts when in active session
    if active_session:
        shortcut_handled = await handleActiveSessionShortcuts(
            message, session, active_session
        )
        if shortcut_handled:
            return

    async with message.channel.typing():
        history = message_history.get(discord_id, [])

        # Add context about active session to help AI understand
        context = ""
        if active_session:
            session_data = active_session.get("session", {})
            routine_name = session_data.get("routine_name", "a routine")
            current_step = session_data.get("current_step_index", 0) + 1
            total_steps = active_session.get("total_steps", 0)
            context = f"\n[Context: User is currently in active session for '{routine_name}', on step {current_step} of {total_steps}. They can say 'done', 'skip', 'pause', 'resume', or 'stop'.]"

        parsed = ai_parser.parse(
            message.content + context, "command_parser", history=history
        )

        if discord_id not in message_history:
            message_history[discord_id] = []
        message_history[discord_id].append((message.content, parsed))
        message_history[discord_id] = message_history[discord_id][-5:]

    if "needs_clarification" in parsed:
        await message.channel.send(
            f"I'm not quite sure what you mean. {parsed['needs_clarification']}"
        )
        return

    if "error" in parsed:
        await message.channel.send(
            f"I had trouble understanding that: {parsed['error']}"
        )
        return

    interaction_type = parsed.get("interaction_type")
    handler = get_handler(interaction_type)

    if handler:
        await handler(message, session, parsed)
    else:
        registered = ", ".join(list_registered()) or "none"
        await message.channel.send(
            f"Unknown command type '{interaction_type}'. Registered modules: {registered}"
        )


@client.event
async def on_ready():
    print(f"Bot logged in as {client.user}")
    loadCache()
    backgroundLoop.start()


@client.event
async def on_message(message):
    if message.author == client.user:
        return
    if not isinstance(message.channel, discord.DMChannel):
        return

    discord_id = message.author.id

    if discord_id in login_state:
        await handleLoginStep(message)
        return

    if discord_id not in user_sessions:
        login_state[discord_id] = {"step": "username"}
        await message.channel.send("Welcome! Send your username to log in.")
        return

    await routeCommand(message)


@tasks.loop(seconds=60)
async def backgroundLoop():
    """Override this in your domain module or extend as needed."""
    pass


@backgroundLoop.before_loop
async def beforeBackgroundLoop():
    await client.wait_until_ready()


if __name__ == "__main__":
    client.run(DISCORD_BOT_TOKEN)
