import { Hono } from "hono"
import { serveStatic } from "hono/bun"
import { render } from "preact-render-to-string"
import { readdirSync } from "fs"
import { Glob } from "bun"
import { mkdir, readdir } from 'node:fs/promises'
import { basename, resolve, join } from 'path'
import { Layout } from "./components/Layout"
import { Project } from "./components/Project"
import { Projects } from "./components/Projects"

export const REPO_URL = "https://github.com/probablycorey/the-workshop"
export const PROJECTS_DIR = ".."
export const WORKSHOP_APP_DOMAIN = "theworkshop.cc"
export const DATA_DIR = process.env.DATA_DIR || "."
export const CUBBY_DIR = join(DATA_DIR, "cubby")

export type ProjectMetadata = {
    name: string
    mtime: Date
    status: "recent" | "active" | "inactive",
    github: string,
    url: string | null,
}

// ----------------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------------

const app = new Hono()
const api = new Hono()

app.use('/css/*', serveStatic({ root: './public' }))
app.use('/js/*', serveStatic({ root: './public' }))
app.use('/src/css/*', serveStatic({ root: '.' }))
app.use('/src/js/*', serveStatic({ root: '.' }))

app.use("*", async (c, next) => {
    const start = Date.now()
    await next()
    const end = Date.now()
    console.log(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`)
})

// ----------------------------------------------------------------------------
// API routes
// ----------------------------------------------------------------------------

api.get('/projects', async c => {
    return c.json(await projectsWithMetadata())
})

// ----------------------------------------------------------------------------
// Web routes
// ----------------------------------------------------------------------------

app.get("/", async (c) => {
    const projects = await projectsWithMetadata()
    return c.html(tsx(<Projects projects={projects} />))
})

app.get("/p/:name", async (c) => {
    const name = c.req.param("name").replace("..", "")
    const project = await projectsWithMetadata().then(projects => projects.find(p => p.name === name))
    if (!project) return c.json({ error: 'Project not found' }, 404)

    let readme = ""
    const glob = new Glob("*.md")
    const projectDir = join(PROJECTS_DIR, name)

    for (const file of glob.scanSync(projectDir)) {
        if (/readme\.md/i.test(file)) {
            readme = await Bun.file(join(projectDir, file)).text()
            break
        }
    }

    const cubbyPath = join(CUBBY_DIR, `project_${name}`)
    let files: string[] = []

    try {
        files = await readdir(cubbyPath)
    } catch (err: any) {
        if (err.code !== "ENOENT") {
            console.error("Error reading cubby:", err)
        }
    }

    return c.html(tsx(<Project project={project} readme={readme} files={files} />))
})

app.get('/p/:id/:filename', async c => {
    const { id, filename } = c.req.param()
    const path = join(CUBBY_DIR, `project_${id}`, filename)
    const file = Bun.file(path)
    if (!(await file.exists())) return c.json({ error: 'Not found' }, 404)

    return new Response(file, {
        headers: {
            'Content-Type': file.type || 'application/octet-stream',
        },
    })
})

app.post('/p/:id/upload', async c => {
    const id = c.req.param('id')

    if (id !== basename(id))
        return c.json({ error: 'Invalid project id' }, 400)

    const folder = join(CUBBY_DIR, `project_${id}`)
    try {
        await mkdir(folder, { recursive: true })

        const form = await c.req.formData()
        const file = form.get('file')
        if (!(file instanceof File))
            return c.json({ error: 'Invalid file' }, 400)

        const filename = basename(file.name)
        const filepath = join(folder, filename)
        await Bun.write(filepath, await file.arrayBuffer())

        return c.redirect(`/p/${id}`)
    } catch (err) {
        console.error('Upload error:', err)
        return c.json({ error: 'Upload failed' }, 500)
    }
})

app.delete('/p/:id/delete/:filename', async c => {
    const { id, filename } = c.req.param()

    if (id !== basename(id) || filename !== basename(filename))
        return c.json({ error: 'Invalid project id or filename' }, 400)

    const filepath = join(CUBBY_DIR, `project_${id}`, filename)
    try {
        await Bun.file(filepath).delete()
        return c.json({ success: true })
    } catch (err) {
        console.error('Delete error:', err)
        return c.json({ error: 'Delete failed' }, 500)
    }
})

// ----------------------------------------------------------------------------
// Helper functions
// ----------------------------------------------------------------------------

async function projects(): Promise<string[]> {
    const subdirs = readdirSync(PROJECTS_DIR, { withFileTypes: true })
        .filter((entry: any) => entry.isDirectory())
        .map((entry: any) => entry.name)
        .filter(name => {
            try {
                return Bun.file(`${PROJECTS_DIR}/${name}/package.json`).size > 0
            } catch {
                return false
            }
        })
        .sort()

    return subdirs
}

async function projectsWithMetadata(): Promise<ProjectMetadata[]> {
    const projects = await projectsWithDates()
    return Promise.all(projects.map(async project => ({
        name: project.name,
        mtime: project.mtime,
        status: project.status,
        github: `${REPO_URL}/tree/main/packages/${project.name}`,
        url: await isWorkshopApp(project.name) ? `https://${project.name}.${WORKSHOP_APP_DOMAIN}` : null,
    })))
}

async function isWorkshopApp(projectName: string): Promise<boolean> {
    const json = await Bun.file(`${PROJECTS_DIR}/${projectName}/package.json`).json()
    return json.scripts && json.scripts["subdomain:start"] !== undefined
}

type ProjectDate = {
    name: string
    mtime: Date
    status: "recent" | "active" | "inactive"
}

async function projectsWithDates(): Promise<ProjectDate[]> {
    const names = await projects()

    // Try to batch GitHub API calls for better performance
    const dates = await batchedMtimes(names)

    const projectsWithDates = names.map((name, index) => {
        const lastModified = dates[index] || new Date(0)
        const daysSince = (Date.now() - lastModified.getTime()) / (24 * 60 * 60 * 1000)
        const status: ProjectDate["status"] = daysSince <= 14 ? "recent" : daysSince <= 30 ? "active" : "inactive"
        return { name, mtime: lastModified, status }
    }).filter((project): project is ProjectDate => project.mtime instanceof Date)

    const sorted = projectsWithDates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
    return sorted
}

// Cache to avoid redundant API calls
const mtimeCache = new Map<string, { date: Date, timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes

async function batchedMtimes(projects: string[]): Promise<Date[]> {
    const token = process.env.GITHUB_TOKEN
    if (!token) {
        console.warn('GITHUB_TOKEN not set, returning default dates')
        return projects.map(() => new Date(0))
    }

    // Check cache for all projects first
    const results: (Date | null)[] = new Array(projects.length).fill(null)
    const uncachedProjects: { index: number, name: string }[] = []

    projects.forEach((project, index) => {
        const cached = mtimeCache.get(project)
        if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
            results[index] = cached.date
        } else {
            uncachedProjects.push({ index, name: project })
        }
    })

    if (uncachedProjects.length === 0) {
        return results as Date[]
    }

    try {
        // Batch API calls with Promise.all for parallel execution
        const apiResults = await Promise.all(uncachedProjects.map(async ({ index, name }) => {
            try {
                console.log(`Fetching GitHub API for ${name}`)
                const response = await fetch(`https://api.github.com/repos/probablycorey/the-workshop/commits?path=packages/${encodeURIComponent(name)}&per_page=1`, {
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        'Accept': 'application/vnd.github.v3+json',
                        'User-Agent': 'cubby-workshop-app'
                    }
                })

                if (!response.ok) {
                    console.warn(`GitHub API error for ${name}: ${response.status}`)
                    return { index, date: new Date(0) }
                }

                const commits = await response.json()
                if (!commits || commits.length === 0) {
                    return { index, date: new Date(0) }
                }

                const date = new Date(commits[0].commit.committer.date)
                mtimeCache.set(name, { date, timestamp: Date.now() })
                return { index, date }
            } catch (error) {
                console.warn(`Error fetching GitHub data for ${name}:`, error)
                return { index, date: new Date(0) }
            }
        }))

        // Fill in the results array
        apiResults.forEach(({ index, date }) => {
            results[index] = date
        })

        return results as Date[]
    } catch (error) {
        console.warn('Batch GitHub API call failed, returning default dates:', error)
        return projects.map(() => new Date(0))
    }
}


function tsx(node: any) {
    return "<!DOCTYPE html>" + render(<Layout>{node}</Layout>)
}

// ----------------------------------------------------------------------------
// Server
// ----------------------------------------------------------------------------

app.route('/api', api)

export default {
    port: process.env.PORT || 3000,
    fetch: app.fetch,
    maxRequestBodySize: 1024 * 1024 * 1024, // 1GB
}