import 'dotenv/config';
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import initSqlJs from 'sql.js';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
import cors from 'cors';

const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });

// Configuration
const PORT = process.env.PORT || 3000;
const LETTERS_CACHE_SIZE = parseInt(process.env.LETTERS_CACHE_SIZE) || 1000;
const LETTER_GENERATION_INTERVAL = parseInt(process.env.LETTER_GENERATION_INTERVAL) || 800;
const MAX_SUBMISSIONS_PER_HOUR = parseInt(process.env.MAX_SUBMISSIONS_PER_HOUR) || 1;
const MAX_UPVOTES_PER_DAY = parseInt(process.env.MAX_UPVOTES_PER_DAY) || 10;
const LOG_LETTERS = process.env.LOG_LETTERS === 'true';
const DB_PATH = process.env.DB_PATH || join(__dirname, 'monkey.db');

let db;

// Initialize SQLite database
async function initDB() {
    const SQL = await initSqlJs();

    if (existsSync(DB_PATH)) {
        const buffer = readFileSync(DB_PATH);
        db = new SQL.Database(buffer);
    } else {
        db = new SQL.Database();
    }

    // Create tables
    db.run(`
        CREATE TABLE IF NOT EXISTS letters (
                                               id INTEGER PRIMARY KEY AUTOINCREMENT,
                                               letter TEXT NOT NULL,
                                               timestamp INTEGER NOT NULL,
                                               created_at INTEGER DEFAULT (strftime('%s', 'now'))
            );
    `);

    db.run(`
        CREATE TABLE IF NOT EXISTS submissions (
                                                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                                                   phrase TEXT NOT NULL,
                                                   timestamps TEXT NOT NULL,
                                                   votes INTEGER DEFAULT 0,
                                                   created_at INTEGER DEFAULT (strftime('%s', 'now'))
            );
    `);

    db.run(`
        CREATE TABLE IF NOT EXISTS submission_votes (
                                                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                                                        submission_id INTEGER NOT NULL,
                                                        ip_address TEXT NOT NULL,
                                                        created_at INTEGER DEFAULT (strftime('%s', 'now')),
            UNIQUE(submission_id, ip_address)
            );
    `);

    db.run(`
        CREATE TABLE IF NOT EXISTS submission_attempts (
                                                           id INTEGER PRIMARY KEY AUTOINCREMENT,
                                                           ip_address TEXT NOT NULL,
                                                           created_at INTEGER DEFAULT (strftime('%s', 'now'))
            );
    `);

    db.run(`
        CREATE TABLE IF NOT EXISTS upvote_attempts (
                                                       id INTEGER PRIMARY KEY AUTOINCREMENT,
                                                       ip_address TEXT NOT NULL,
                                                       created_at INTEGER DEFAULT (strftime('%s', 'now'))
            );
    `);

    // Indexes for rate-limit queries (ip_address + created_at lookups)
    db.run('CREATE INDEX IF NOT EXISTS idx_sub_attempts_ip ON submission_attempts (ip_address, created_at)');
    db.run('CREATE INDEX IF NOT EXISTS idx_upvote_attempts_ip ON upvote_attempts (ip_address, created_at)');
    db.run('CREATE INDEX IF NOT EXISTS idx_sub_votes_lookup ON submission_votes (submission_id, ip_address)');
    db.run('CREATE INDEX IF NOT EXISTS idx_letters_timestamp ON letters (timestamp)');

    saveDB();
}

function saveDB() {
    const data = db.export();
    const buffer = Buffer.from(data);
    writeFileSync(DB_PATH, buffer);
}

// Middleware
app.use(cors());
app.use(express.json());
app.set('trust proxy', 1);

function getClientIP(req) {
    return req.ip || req.socket.remoteAddress;
}

// Generate random letter
function generateRandomLetter() {
    const chars = 'abcdefghijklmnopqrstuvwxyz ';
    return chars[Math.floor(Math.random() * chars.length)];
}

let letterInterval = null;
let letterCount = 0;

// Purge stale rate-limit records — these are only needed within their time window
function purgeOldRateLimitRecords() {
    const now = Math.floor(Date.now() / 1000);
    db.run('DELETE FROM submission_attempts WHERE created_at < ?', [now - 3600]);
    db.run('DELETE FROM upvote_attempts WHERE created_at < ?', [now - 86400]);
}

// Generate and broadcast letters
function startLetterGeneration() {
    if (letterInterval) {
        clearInterval(letterInterval);
    }

    letterInterval = setInterval(() => {
        const letter = generateRandomLetter();
        const timestamp = Date.now();

        db.run('INSERT INTO letters (letter, timestamp) VALUES (?, ?)', [letter, timestamp]);

        if (LOG_LETTERS) console.log(`Letter: "${letter}"`);

        const message = JSON.stringify({ type: 'letter', letter, timestamp });
        wss.clients.forEach(client => {
            if (client.readyState === 1) {
                client.send(message);
            }
        });

        // Clean old letters and save every 100 ticks (~80 seconds at default interval)
        letterCount++;
        if (letterCount % 100 === 0) {
            db.run(`
                DELETE FROM letters
                WHERE id NOT IN (
                    SELECT id FROM letters ORDER BY id DESC LIMIT ?
                )
            `, [LETTERS_CACHE_SIZE]);
            purgeOldRateLimitRecords();
            saveDB();
        }
    }, LETTER_GENERATION_INTERVAL);
}

// WebSocket connection
wss.on('connection', (ws) => {
    ws.send(JSON.stringify({ type: 'init', letters: [] }));

    if (wss.clients.size === 1) {
        startLetterGeneration();
    }

    ws.on('close', () => {
        if (wss.clients.size === 0) {
            clearInterval(letterInterval);
            letterInterval = null;
        }
    });
});

// API Routes

// Submit phrase
app.post('/api/submit', (req, res) => {
    const { phrase, timestamps } = req.body;
    const ip = getClientIP(req);

    if (!phrase || !timestamps || !Array.isArray(timestamps)) {
        return res.status(400).json({ error: 'Invalid request' });
    }

    // Check rate limit
    const now = Math.floor(Date.now() / 1000);
    const oneHourAgo = now - 3600;
    const countResult = db.exec(`
        SELECT COUNT(*) as count FROM submission_attempts
        WHERE ip_address = ? AND created_at > ?
    `, [ip, oneHourAgo]);

    const count = countResult[0]?.values[0]?.[0] || 0;
    if (count >= MAX_SUBMISSIONS_PER_HOUR) {
        return res.status(429).json({
            error: `Maximum ${MAX_SUBMISSIONS_PER_HOUR} submission(s) per hour reached`
        });
    }

    // Validate phrase
    if (phrase.length !== timestamps.length || phrase.length === 0 || phrase.length > 200) {
        return res.status(400).json({ error: 'Invalid phrase length' });
    }

    // Check for duplicate phrase
    const dupeResult = db.exec(
        'SELECT COUNT(*) as count FROM submissions WHERE LOWER(TRIM(phrase)) = LOWER(TRIM(?))',
        [phrase.trim()]
    );
    const dupeCount = dupeResult[0]?.values[0]?.[0] || 0;
    if (dupeCount > 0) {
        return res.status(400).json({ error: 'This phrase already exists on the leaderboard' });
    }

    // Validate all letters in one query
    const placeholders = timestamps.map(() => '?').join(',');
    const letterResult = db.exec(
        `SELECT timestamp, letter FROM letters WHERE timestamp IN (${placeholders})`,
        timestamps
    );
    const validLetters = new Map();
    if (letterResult.length && letterResult[0].values.length) {
        for (const [ts, lt] of letterResult[0].values) {
            validLetters.set(ts, lt);
        }
    }
    for (let i = 0; i < timestamps.length; i++) {
        if (validLetters.get(timestamps[i]) !== phrase[i]) {
            return res.status(400).json({
                error: 'Invalid submission - letters do not match server records'
            });
        }
    }

    // Insert submission
    db.run('INSERT INTO submissions (phrase, timestamps) VALUES (?, ?)',
        [phrase.trim(), JSON.stringify(timestamps)]);

    const lastIdResult = db.exec('SELECT last_insert_rowid() as id');
    const submissionId = lastIdResult[0].values[0][0];

    db.run('INSERT INTO submission_attempts (ip_address) VALUES (?)', [ip]);
    saveDB();

    res.json({
        success: true,
        submission: {
            id: submissionId,
            phrase: phrase.trim(),
            votes: 0
        }
    });
});

// Get leaderboard
app.get('/api/leaderboard', (req, res) => {
    const result = db.exec(`
        SELECT id, phrase, votes, created_at
        FROM submissions
        ORDER BY votes DESC, created_at DESC
            LIMIT 100
    `);

    let submissions = [];
    if (result.length > 0 && result[0].values.length > 0) {
        submissions = result[0].values.map(row => ({
            id: row[0],
            phrase: row[1],
            votes: row[2],
            created_at: row[3]
        }));
    }

    res.json(submissions);
});

// Upvote
app.post('/api/upvote', (req, res) => {
    const { id } = req.body;
    const ip = getClientIP(req);

    if (!id) {
        return res.status(400).json({ error: 'Invalid request' });
    }

    // Check rate limit
    const now = Math.floor(Date.now() / 1000);
    const oneDayAgo = now - 86400;
    const countResult = db.exec(`
        SELECT COUNT(*) as count FROM upvote_attempts
        WHERE ip_address = ? AND created_at > ?
    `, [ip, oneDayAgo]);

    const count = countResult[0]?.values[0]?.[0] || 0;
    if (count >= MAX_UPVOTES_PER_DAY) {
        return res.status(429).json({
            error: `Maximum ${MAX_UPVOTES_PER_DAY} upvotes per day reached`
        });
    }

    // Check if submission exists
    const subResult = db.exec('SELECT 1 FROM submissions WHERE id = ?', [id]);
    if (!subResult.length || !subResult[0].values.length) {
        return res.status(404).json({ error: 'Submission not found' });
    }

    // Check if already voted
    const voteResult = db.exec(
        'SELECT 1 FROM submission_votes WHERE submission_id = ? AND ip_address = ?',
        [id, ip]
    );

    if (voteResult.length && voteResult[0].values.length) {
        return res.status(400).json({ error: 'Already voted for this submission' });
    }

    // Add vote
    try {
        db.run('INSERT INTO submission_votes (submission_id, ip_address) VALUES (?, ?)', [id, ip]);
        db.run('UPDATE submissions SET votes = votes + 1 WHERE id = ?', [id]);
        db.run('INSERT INTO upvote_attempts (ip_address) VALUES (?)', [ip]);
        saveDB();

        const updated = db.exec('SELECT votes FROM submissions WHERE id = ?', [id]);
        const votes = updated[0].values[0][0];

        res.json({ success: true, votes });
    } catch (error) {
        res.status(500).json({ error: 'Failed to record vote' });
    }
});

// Serve React build in production
const clientBuild = join(__dirname, 'public');
if (existsSync(clientBuild)) {
    app.use(express.static(clientBuild));
    app.get('*', (req, res) => {
        res.sendFile(join(clientBuild, 'index.html'));
    });
}

// Start server
initDB().then(() => {
    // Run cleanup once an hour regardless of letter generation activity
    setInterval(() => {
        purgeOldRateLimitRecords();
        saveDB();
    }, 60 * 60 * 1000);

    server.listen(PORT, () => {
        console.log(`Server running on port ${PORT}`);
    });
}).catch(err => {
    console.error('Failed to initialize database:', err);
    process.exit(1);
});