import discord, asyncio
import os, sys, traceback
import random, string, json
from discord.ext import commands, tasks

# Bot setup with necessary intents
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="br!", intents=intents)

# Settings
MAX_FOLDER_SIZE = 10 * 1024 * 1024 * 1024  # teen gigga bytes
MAX_FILE_SIZE = 512 * 1024 * 1024  # 512MB in bytes
DOWNLOAD_FOLDER = "billionreservoir"
evaluser = 798072830595301406
descchannel = 0

# Ensure download folder exists
if not os.path.exists(DOWNLOAD_FOLDER):
    os.makedirs(DOWNLOAD_FOLDER)

def get_folder_size():
    """Calculates the total size of the downloads folder."""
    return sum(os.path.getsize(os.path.join(DOWNLOAD_FOLDER, f)) for f in os.listdir(DOWNLOAD_FOLDER) if os.path.isfile(os.path.join(DOWNLOAD_FOLDER, f)))

def generate_random_string(length=6):
    """Generates a random string of given length."""
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

def get_unique_filename(filename):
    """Ensures unique filename by appending a random string if necessary."""
    file_path = os.path.join(DOWNLOAD_FOLDER, filename)
    while os.path.exists(file_path):
        name, ext = os.path.splitext(filename)
        filename = f"{name}_{generate_random_string()}{ext}"
        file_path = os.path.join(DOWNLOAD_FOLDER, filename)
    return file_path, filename

# Load existing data from db.json
def load_db():
    try:
        with open(f"db.json", 'r') as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

# Save data to db.json
def save_db(data):
    with open(f"db.json", 'w') as f:
        json.dump(data, f, indent=4)

def format_size(size_bytes):
    units = ["B", "KiB", "MiB", "GiB", "TiB"]
    value = float(size_bytes)
    unit_index = 0

    while value >= 1024 and unit_index < len(units) - 1:
        value /= 1024
        unit_index += 1

    return f"{value:.2f}{units[unit_index]}"

async def update_channel_description():
    """Updates the channel description with storage details every 10 minutes."""
    channel = bot.get_channel(descchannel)
    if channel:
        used_space = get_folder_size()
        free_space = MAX_FOLDER_SIZE - used_space
        description = (
            f"Storage used: {format_size(used_space)}/{format_size(MAX_FOLDER_SIZE)}\n"
            f"Free space: {format_size(free_space)}\n\n"
            f"[rich snob voice] I think today I'll use my \"Billion Reservoir\" SSD. Only the finest of storage for the finest of prison laptops. 🧐☕🇬🇧👑"
        )
        await channel.edit(topic=description)
    print("updated channel description")

@tasks.loop(minutes=10)
async def periodic_update():
    await update_channel_description()

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")
    periodic_update.start()

@bot.check
async def allow_bots(ctx):
    return True  # Allow bots to invoke commands

@bot.event
async def on_message(message):
    if "br!ig" in message.content:
        return

    db = load_db()
    db.setdefault("channels", [])
    channels = db["channels"]
    global descchannel
    descchannel = channels[0]

    """Handles file uploads, enforces storage limits, and adds reactions."""
    ctx = await bot.get_context(message)
    await bot.invoke(ctx)
    if not message.channel.id in channels:
        return

    reactions = []
    if message.attachments:
        for attachment in message.attachments:
            #check if file is php
            if attachment.filename.lower().endswith('.php'):
                await message.channel.send(f"sorry but im not gonna let you do a really funny ace exploit 🤓")
                reactions.append("❌")
                print(f"❌ {attachment.filename} rejected (blocked extension)")
                continue

            # Check if the file size is too large
            if attachment.size > MAX_FILE_SIZE:
                await message.channel.send(f"The file '{attachment.filename}' is too large for the destination file system.\n-# Max size is {MAX_FILE_SIZE / (1024 * 1024):.2f}MB")
                reactions.append("❌")
                print(f"❌ {attachment.filename} rejected (too large: {attachment.size / (1024 * 1024):.2f}MB)")
                continue
            
            # Check if adding this file exceeds the total folder limit
            current_size = get_folder_size()
            if current_size + attachment.size > MAX_FOLDER_SIZE:
                await message.channel.send(f"<:Error3:1504309281992872098> Disk Full\n\nThe Operation could not be completed because not enough space is available on the disk.")
                reactions.append("❌")
                print(f"❌ {attachment.filename} rejected (not enough space: {current_size / (1024 * 1024):.2f}MB used)")
                continue

            # Get unique filename
            file_path, unique_filename = get_unique_filename(attachment.filename)
            
            # Download the file
            try:
                await attachment.save(file_path)
                reactions.append("✅")
                print(f"✅ Downloaded {unique_filename} ({attachment.size / (1024 * 1024):.2f}MB)")
            except Exception as e:
                await message.channel.send(f"<:Error3:1504309281992872098> Downloading file ({attachment.filename}) failed: {e}")
                reactions.append("❌")
                print(f"❌ Failed to download {attachment.filename}: {e}")
    
    # Add reaction only once after processing all files
    if reactions:
        await message.add_reaction(reactions[-1])

@bot.command()
async def ls(ctx, page: int = 1):
    """Lists the files with pagination."""
    files = sorted(os.listdir(DOWNLOAD_FOLDER))
    if not files:
        await ctx.send("No files available.")
        return
    
    files_per_page = 15
    total_pages = (len(files) + files_per_page - 1) // files_per_page
    
    if page < 1 or page > total_pages:
        await ctx.send(f"Invalid page number. Please select between 1 and {total_pages}.")
        return
    
    start_idx = (page - 1) * files_per_page
    end_idx = start_idx + files_per_page
    
    file_list = "\n".join([f"{f} - {os.path.getsize(os.path.join(DOWNLOAD_FOLDER, f)) / 1024:.2f} KB" for f in files[start_idx:end_idx]])
    
    await ctx.send(f"```Stored files (Page {page}/{total_pages}):\n{file_list}```")

@bot.command()
async def sc(ctx, *, query: str):
    """Searches for files matching the query."""
    files = [f for f in os.listdir(DOWNLOAD_FOLDER) if query.lower() in f.lower()]
    if files:
        await ctx.send(f"Matching files:\n```\n" + "\n".join(files) + "\n```")
    else:
        await ctx.send("No matching files found.")

@bot.command()
async def st(ctx, *, status: str):
    """Sets the bot's status."""
    await bot.change_presence(activity=discord.CustomActivity(name=status))
    await ctx.send(f"Status updated to: {status}")

@bot.command()
async def dl(ctx, filename: str):
    """DMs the user the requested file."""
    file_path = os.path.join(DOWNLOAD_FOLDER, filename)
    if os.path.exists(file_path) and os.path.isfile(file_path):
        await ctx.author.send(file=discord.File(file_path))
        await ctx.send(f"{ctx.author.mention}, file sent to your DMs.")
    else:
        await ctx.send("File not found.")

@bot.command()
async def fr(ctx):
    """Gets Free Space"""
    used_space = get_folder_size()
    free_space = MAX_FOLDER_SIZE - used_space
    description = (
        f"Storage used: {format_size(used_space)}/{format_size(MAX_FOLDER_SIZE)}\n"
        f"Free space: {format_size(free_space)}"
    )
    await ctx.send(description)

@bot.command()
async def ad(ctx):
    """(trusted only) Add channel to database"""
    db = load_db()
    db.setdefault("channels", [])
    db.setdefault("trustedinstallers", [])
    if not ctx.author.id in dbs["trustedinstallers"]:
        return await ctx.send("no permission")
    if ctx.channel.id in dbs["channels"]:
        return await ctx.send("already added")
    db["channels"].append(ctx.channel.id)
    save_db(db)
    await ctx.send("added channel to db")

@bot.command()
async def rm(ctx):
    """(trusted only) Unadd channel to database"""
    db = load_db()
    db.setdefault("channels", [])
    db.setdefault("trustedinstallers", [])
    if not ctx.author.id in dbs["trustedinstallers"]:
        return await ctx.send("no permission")
    if not ctx.channel.id in dbs["channels"]:
        return await ctx.send("not in database")
    db["channels"].remove(ctx.channel.id)
    save_db(db)
    await ctx.send("unadded channel to db")

@bot.command(help="(owner only) restarts the bot")
async def rs(ctx):
    if ctx.author.id == evaluser:
        console_log("restart has been triggered...")
        await ctx.send("restarting bot...")
        os.execv(sys.executable, ['python'] + sys.argv)

@bot.command(help="and cat bot has a level 100 skibidi sigma mafia boss eval command")
async def ev(ctx, *, prompt: str):
    if ctx.author.id == evaluser:
        # complex eval, multi-line + async support
        # requires the full `await message.channel.send(2+3)` to get the result
        # thanks mia lilenakos
        spaced = ""
        for i in prompt.split("\n"):
            spaced += "  " + i + "\n"

        intro = (
            "async def go(prompt, bot, ctx):\n"
            " try:\n"
        )
        ending = (
            "\n except Exception:\n"
            "  await ctx.send(traceback.format_exc())"
            "\nbot.loop.create_task(go(prompt, bot, ctx))"
        )

        complete = intro + spaced + ending
        exec(complete)

bot.run('')
