#
# Copyright (c) 2025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
import os
from contextlib import asynccontextmanager
from typing import Any, Dict, List

import aiohttp
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pipecat.transports.daily.utils import DailyRESTHelper, DailyRoomParams

# Load environment variables
load_dotenv(override=True)

NUMBER_OF_ROOMS = 1


class RoomPool:
    """Manages a pool of pre-created rooms for quick allocation."""

    def __init__(self, daily_rest_helper: DailyRESTHelper):
        self.daily_rest_helper = daily_rest_helper
        self.pool: List[Dict[str, str]] = []
        self.lock = asyncio.Lock()

    async def fill_pool(self, count: int):
        """Fills the pool with `count` new rooms."""
        for _ in range(count):
            await self.add_room()

    async def add_room(self):
        """Creates a new room and adds it to the pool."""
        try:
            room = await self.daily_rest_helper.create_room(DailyRoomParams())
            if not room.url:
                raise HTTPException(status_code=500, detail="Failed to create room")

            user_token = await self.daily_rest_helper.get_token(room.url)
            if not user_token:
                raise HTTPException(status_code=500, detail="Failed to get user token")

            bot_token = await self.daily_rest_helper.get_token(room.url)
            if not bot_token:
                raise HTTPException(status_code=500, detail="Failed to get bot token")

            async with self.lock:
                self.pool.append(
                    {"room_url": room.url, "user_token": user_token, "bot_token": bot_token}
                )

        except Exception as e:
            print(f"Error adding room to pool: {e}")

    async def get_room(self) -> Dict[str, str]:
        """Retrieves a room from the pool and requests a new one to maintain the size."""
        async with self.lock:
            if not self.pool:
                raise HTTPException(status_code=503, detail="No available rooms")

            room = self.pool.pop(0)  # Get first available room

        # Start a background task to replenish the pool
        asyncio.create_task(self.add_room())

        return room

    async def delete_room(self, room_url: str):
        """Deletes a room when it is not needed anymore"""
        await self.daily_rest_helper.delete_room_by_url(room_url)

    async def cleanup(self):
        for rooms in self.pool:
            room_url = rooms["room_url"]
            await self.delete_room(room_url)


class BotManager:
    """Manages bot subprocesses asynchronously."""

    def __init__(self):
        self.bot_procs: Dict[int, asyncio.subprocess.Process] = {}
        self.room_mappings: Dict[int, str] = {}  # Maps process ID to room URL

    async def start_bot(self, room_url: str, token: str) -> int:
        bot_file = "single_bot"
        command = f"python3 -m {bot_file} -u {room_url} -t {token}"

        try:
            proc = await asyncio.create_subprocess_shell(
                command,
                cwd=os.path.dirname(os.path.abspath(__file__)),
            )
            if proc.pid is None:
                raise HTTPException(status_code=500, detail="Failed to get subprocess PID")

            self.bot_procs[proc.pid] = proc
            self.room_mappings[proc.pid] = room_url
            # Monitor the process and delete the room when it exits
            asyncio.create_task(self._monitor_process(proc.pid))

            return proc.pid
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}")

    async def _monitor_process(self, pid: int):
        """Monitors a bot process and deletes the associated room when it exits."""
        proc = self.bot_procs.get(pid)
        if proc:
            await proc.wait()  # Wait for the process to exit
            room_url = self.room_mappings.pop(pid, None)

            if room_url:
                await room_pool.delete_room(room_url)
                print(f"Deleted room: {room_url}")

            del self.bot_procs[pid]

    async def cleanup(self):
        """Terminates all running bot processes and deletes associated rooms."""
        for pid, proc in list(self.bot_procs.items()):
            try:
                proc.terminate()
                await asyncio.wait_for(proc.wait(), timeout=5)

                room_url = self.room_mappings.pop(pid, None)
                if room_url:
                    await room_pool.delete_room(room_url)  # Delete room when process terminates
                    print(f"Deleted room: {room_url}")

            except asyncio.TimeoutError:
                print(f"Process {pid} did not terminate in time.")
            except Exception as e:
                print(f"Error terminating process {pid}: {e}")

        # Clear remaining mappings
        self.bot_procs.clear()
        self.room_mappings.clear()


# Global instances
bot_manager = BotManager()
room_pool: RoomPool  # Will be initialized in lifespan


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Handles FastAPI startup and shutdown."""
    global room_pool
    aiohttp_session = aiohttp.ClientSession()
    daily_rest_helper = DailyRESTHelper(
        daily_api_key=os.getenv("DAILY_API_KEY", ""),
        daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
        aiohttp_session=aiohttp_session,
    )

    room_pool = RoomPool(daily_rest_helper)
    await room_pool.fill_pool(NUMBER_OF_ROOMS)  # Fill pool on startup

    yield  # Run app

    await bot_manager.cleanup()
    await room_pool.cleanup()
    await aiohttp_session.close()


# Initialize FastAPI app with lifespan manager
app = FastAPI(lifespan=lifespan)

# Configure CORS to allow requests from any origin
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.post("/connect")
async def bot_connect(request: Request) -> Dict[Any, Any]:
    try:
        room = await room_pool.get_room()
        await bot_manager.start_bot(room["room_url"], room["bot_token"])
    except HTTPException as e:
        return {"error": str(e)}

    return {
        "room_url": room["room_url"],
        "token": room["user_token"],
    }


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)
