// --- Imports ---
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const multer = require('multer');
const Papa = require('papaparse');
const fs = require('fs');

// --- Config ---
const app = express();
const port = 3000;
const dbFile = './simpleledger.db';
const JWT_SECRET = 'your-super-secret-key-change-this!'; // CHANGE THIS
const upload = multer({ dest: 'uploads/' });

// --- Middleware ---
app.use(cors());
app.use(express.json());
// Serve the frontend file
app.use(express.static(__dirname)); 

// --- Database Setup ---
const db = new sqlite3.Database(dbFile, (err) => {
    if (err) {
        console.error(err.message);
    } else {
        console.log('Connected to the SimpleLedger SQLite database.');
        initDb();
    }
});

function initDb() {
    db.serialize(() => {
        db.run(`CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT UNIQUE,
            password TEXT
        )`);
        
        db.run(`CREATE TABLE IF NOT EXISTS categories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            userId INTEGER,
            name TEXT,
            type TEXT,
            FOREIGN KEY(userId) REFERENCES users(id)
        )`);
        
        db.run(`CREATE TABLE IF NOT EXISTS vendors (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            userId INTEGER,
            name TEXT UNIQUE,
            FOREIGN KEY(userId) REFERENCES users(id)
        )`);
        
        db.run(`CREATE TABLE IF NOT EXISTS rules (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            userId INTEGER,
            keyword TEXT,
            category TEXT,
            FOREIGN KEY(userId) REFERENCES users(id)
        )`);
        
        db.run(`CREATE TABLE IF NOT EXISTS transactions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            userId INTEGER,
            date TEXT,
            description TEXT,
            amount REAL,
            category TEXT,
            vendorId INTEGER,
            source TEXT,
            createdAt TEXT,
            FOREIGN KEY(userId) REFERENCES users(id),
            FOREIGN KEY(vendorId) REFERENCES vendors(id)
        )`);
    });
}

// --- Auth Middleware ---
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (token == null) return res.sendStatus(401); // No token

    jwt.verify(token, JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // Invalid token
        req.user = user;
        next();
    });
}

// === AUTH ENDPOINTS ===

app.post('/api/register', async (req, res) => {
    const { username, password } = req.body;
    if (!username || !password) {
        return res.status(400).send('Username and password are required.');
    }
    
    try {
        const hashedPassword = await bcrypt.hash(password, 10);
        
        // Use serialize to ensure user is created before categories are added
        db.serialize(() => {
            // 1. Create the user
            db.run('INSERT INTO users (username, password) VALUES (?, ?)', [username, hashedPassword], function(err) {
                if (err) {
                    return res.status(400).send('Username already taken.');
                }
                
                const newUserId = this.lastID;
                
                // 2. Populate default categories for the new user
                const defaultCategories = [
                    { name: 'Income - Gross Sales', type: 'income' },
                    { name: 'Income - Returns', type: 'income' },
                    { name: 'Expense - Uncategorized', type: 'expense' },
                    { name: 'Expense - Advertising', type: 'expense' },
                    { name: 'Expense - Bank Fees', type: 'expense' },
                    { name: 'Expense - Cost of Goods Sold', type: 'expense' },
                    { name: 'Expense - Discounts & Comps', type: 'expense' },
                    { name: 'Expense - Meals & Entertainment', type: 'expense' },
                    { name: 'Expense - Office Supplies', type: 'expense' },
                    { name: 'Expense - Payroll', type: 'expense' },
                    { name: 'Expense - Rent/Lease', type: 'expense' },
                    { name: 'Expense - Software/Subscriptions', type: 'expense' },
                    { name: 'Expense - Taxes & Licenses', type: 'expense' },
                    { name: 'Expense - Utilities', type: 'expense' },
                    { name: 'Expense - Vehicle', type: 'expense' },
                    { name: 'Liability - Sales Tax', type: 'liability' }
                ];

                const stmt = db.prepare('INSERT INTO categories (userId, name, type) VALUES (?, ?, ?)');
                
                db.run('BEGIN TRANSACTION');
                defaultCategories.forEach(cat => {
                    stmt.run(newUserId, cat.name, cat.type);
                });
                db.run('COMMIT', (commitErr) => {
                    stmt.finalize();
                    if (commitErr) {
                        console.error("Error populating default categories:", commitErr);
                        // User was created, but categories failed.
                        // Still send success for registration but log the error.
                    }
                    // Send success response AFTER all DB operations
                    res.status(201).send('User created. Please log in.');
                });
            });
        });

    } catch (error) {
        res.status(500).send('Server error.');
    }
});

app.post('/api/login', (req, res) => {
    const { username, password } = req.body;
    
    db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
        if (err || !user) {
            return res.status(400).send('Cannot find user.');
        }
        
        try {
            if (await bcrypt.compare(password, user.password)) {
                // User is valid, create JWT
                const accessToken = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '24h' });
                res.json({ accessToken: accessToken });
            } else {
                res.status(400).send('Invalid password.');
            }
        } catch (error) {
            res.status(500).send('Server error.');
        }
    });
});

// === API ENDPOINTS (Protected) ===

// --- Categories ---
app.get('/api/categories', authenticateToken, (req, res) => {
    db.all('SELECT * FROM categories WHERE userId = ? ORDER BY name', [req.user.id], (err, rows) => {
        if (err) return res.status(500).send(err);
        res.json(rows);
    });
});

app.post('/api/categories', authenticateToken, (req, res) => {
    const { name, type } = req.body;
    db.run('INSERT INTO categories (userId, name, type) VALUES (?, ?, ?)', [req.user.id, name, type], function(err) {
        if (err) return res.status(500).send(err);
        res.status(201).json({ id: this.lastID, userId: req.user.id, name, type });
    });
});

app.delete('/api/categories/:id', authenticateToken, (req, res) => {
    db.run('DELETE FROM categories WHERE id = ? AND userId = ?', [req.params.id, req.user.id], function(err) {
        if (err) return res.status(500).send(err);
        if (this.changes === 0) return res.status(404).send('Category not found or user unauthorized.');
        res.sendStatus(204);
    });
});

// --- Vendors ---
app.get('/api/vendors', authenticateToken, (req, res) => {
    db.all('SELECT * FROM vendors WHERE userId = ? ORDER BY name', [req.user.id], (err, rows) => {
        if (err) return res.status(500).send(err);
        res.json(rows);
    });
});

app.post('/api/vendors', authenticateToken, (req, res) => {
    const { name } = req.body;
    if (!name) return res.status(400).send('Vendor name is required.');
    
    db.run('INSERT INTO vendors (userId, name) VALUES (?, ?)', [req.user.id, name], function(err) {
        if (err) {
            if (err.message.includes('UNIQUE constraint')) {
                return res.status(400).send('Vendor name must be unique.');
            }
            return res.status(500).send(err);
        }
        res.status(201).json({ id: this.lastID, userId: req.user.id, name });
    });
});

app.delete('/api/vendors/:id', authenticateToken, (req, res) => {
    // TODO: We should check if this vendor is associated with any transactions
    // For now, we'll just delete.
    db.run('DELETE FROM vendors WHERE id = ? AND userId = ?', [req.params.id, req.user.id], function(err) {
        if (err) return res.status(500).send(err);
        if (this.changes === 0) return res.status(404).send('Vendor not found or user unauthorized.');
        res.sendStatus(204);
    });
});


// --- Rules ---
app.get('/api/rules', authenticateToken, (req, res) => {
    db.all('SELECT * FROM rules WHERE userId = ?', [req.user.id], (err, rows) => {
        if (err) return res.status(500).send(err);
        res.json(rows);
    });
});

app.post('/api/rules', authenticateToken, (req, res) => {
    const { keyword, category } = req.body;
    db.run('INSERT INTO rules (userId, keyword, category) VALUES (?, ?, ?)', [req.user.id, keyword, category], function(err) {
        if (err) return res.status(500).send(err);
        res.status(201).json({ id: this.lastID, userId: req.user.id, keyword, category });
    });
});

// *** NEW ENDPOINT ***
app.post('/api/rules/apply-all', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    
    try {
        // 1. Get all rules for the user
        const rules = await new Promise((resolve, reject) => {
            db.all('SELECT * FROM rules WHERE userId = ?', [userId], (err, rows) => {
                if (err) reject(err);
                resolve(rows);
            });
        });

        if (rules.length === 0) {
            return res.status(200).send('No rules to apply.');
        }

        // 2. Get all uncategorized transactions
        const uncategorizedTransactions = await new Promise((resolve, reject) => {
            db.all("SELECT * FROM transactions WHERE userId = ? AND (category = 'Uncategorized' OR category = 'Expense - Uncategorized')", [userId], (err, rows) => {
                if (err) reject(err);
                resolve(rows);
            });
        });
        
        if (uncategorizedTransactions.length === 0) {
            return res.status(200).send('No uncategorized transactions to update.');
        }

        // 3. Apply rules and update
        let updatedCount = 0;
        const updateStmt = db.prepare('UPDATE transactions SET category = ? WHERE id = ?');
        
        db.run('BEGIN TRANSACTION');
        for (const tx of uncategorizedTransactions) {
            const desc = tx.description.toUpperCase();
            for (const rule of rules) {
                if (desc.includes(rule.keyword.toUpperCase())) {
                    updateStmt.run(rule.category, tx.id);
                    updatedCount++;
                    break; // Stop after first matching rule
                }
            }
        }
        db.run('COMMIT', (err) => {
            if (err) {
                console.error("Error committing rule updates:", err);
                return res.status(500).send('Error updating transactions.');
            }
            
            updateStmt.finalize();
            res.status(200).send(`Successfully updated ${updatedCount} transactions.`);
        });

    } catch (error) {
        console.error("Error applying rules:", error);
        res.status(500).send('Server error while applying rules.');
    }
});
// *** END NEW ENDPOINT ***

app.delete('/api/rules/:id', authenticateToken, (req, res) => {
    db.run('DELETE FROM rules WHERE id = ? AND userId = ?', [req.params.id, req.user.id], function(err) {
        if (err) return res.status(500).send(err);
        if (this.changes === 0) return res.status(404).send('Rule not found or user unauthorized.');
        res.sendStatus(204);
    });
});

// --- Transactions ---
app.get('/api/transactions', authenticateToken, (req, res) => {
    db.all('SELECT t.*, v.name as vendorName FROM transactions t LEFT JOIN vendors v ON t.vendorId = v.id WHERE t.userId = ? ORDER BY date DESC', [req.user.id], (err, rows) => {
        if (err) return res.status(500).send(err);
        // Convert dates from text
        rows.forEach(r => r.date = new Date(r.date));
        res.json(rows);
    });
});

app.put('/api/transactions/:id', authenticateToken, (req, res) => {
    const { date, description, amount, category, vendorId } = req.body;
    
    if (!date || !description || isNaN(parseFloat(amount)) || !category) {
        return res.status(400).send('Missing required fields.');
    }
    
    db.run(`UPDATE transactions 
            SET date = ?, description = ?, amount = ?, category = ?, vendorId = ? 
            WHERE id = ? AND userId = ?`, 
            [date, description, amount, category, vendorId || null, req.params.id, req.user.id], 
            function(err) {
        if (err) return res.status(500).send(err);
        if (this.changes === 0) return res.status(404).send('Transaction not found or user unauthorized.');
        res.sendStatus(200);
    });
});

app.delete('/api/transactions/:id', authenticateToken, (req, res) => {
    db.run('DELETE FROM transactions WHERE id = ? AND userId = ?', [req.params.id, req.user.id], function(err) {
        if (err) return res.status(500).send(err);
        if (this.changes === 0) return res.status(404).send('Transaction not found or user unauthorized.');
        res.sendStatus(204);
    });
});

// --- Dashboard ---
app.get('/api/dashboard-summary', authenticateToken, (req, res) => {
    const userId = req.user.id;
    let summary = {
        stats: {},
        pieData: {}
    };

    const statsQuery = `
        SELECT 
            (SELECT SUM(amount) FROM transactions WHERE userId = ? AND amount > 0 AND category LIKE 'Income%') as totalIncome,
            (SELECT SUM(amount) FROM transactions WHERE userId = ? AND amount < 0 AND category LIKE 'Income%') as totalReturns,
            (SELECT SUM(amount) FROM transactions WHERE userId = ? AND amount < 0 AND category LIKE 'Expense%') as totalExpenses
    `;
    
    const pieQuery = `
        SELECT category, SUM(amount) as total 
        FROM transactions 
        WHERE userId = ? AND amount < 0 AND category LIKE 'Expense%'
        GROUP BY category
        ORDER BY total ASC
        LIMIT 10
    `;

    db.get(statsQuery, [userId, userId, userId], (err, stats) => {
        if (err) return res.status(500).send(err);
        summary.stats = {
            totalIncome: stats.totalIncome || 0,
            totalReturns: stats.totalReturns || 0,
            totalExpenses: stats.totalExpenses || 0
        };

        db.all(pieQuery, [userId], (err, pieRows) => {
            if (err) return res.status(500).send(err);
            summary.pieData = {
                labels: pieRows.map(r => r.category),
                data: pieRows.map(r => Math.abs(r.total))
            };
            res.json(summary);
        });
    });
});

// --- File Upload & Processing ---
app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', maxCount: 1 }, { name: 'salesFile', maxCount: 1 }]), async (req, res) => {
    const userId = req.user.id;
    let transactionsToInsert = [];

    // 1. Get current rules from DB
    const rules = await new Promise((resolve, reject) => {
        db.all('SELECT * FROM rules WHERE userId = ?', [userId], (err, rows) => {
            if (err) reject(err);
            resolve(rows);
        });
    });

    // Helper: Apply categorization rules
    const applyCategorizationRules = (tx) => {
        if (tx.category && tx.category !== 'Uncategorized') return tx;
        const desc = tx.description.toUpperCase();
        for (const rule of rules) {
            if (desc.includes(rule.keyword.toUpperCase())) {
                tx.category = rule.category;
                return tx;
            }
        }
        return tx;
    };

    // Helper: Process Bank CSV
    const processBankCSV = (data) => {
        data.forEach(row => {
            const date = new Date(row.Date);
            const debit = parseFloat(row.Debit) || 0;
            const credit = parseFloat(row.Credit) || 0;
            const amount = credit - debit;
            
            if (!isNaN(date) && amount !== 0) {
                transactionsToInsert.push(applyCategorizationRules({
                    date: date,
                    description: row.Description,
                    amount: amount,
                    category: 'Uncategorized',
                    source: 'bank_csv',
                }));
            }
        });
    };
    
    // Helper: Process Sales CSV
    const processSalesCSV = (data, fileName) => {
        let salesData = {};
        const dateMatch = fileName.match(/(\d{4}-\d{2}-\d{2})/);
        const txDate = dateMatch ? new Date(dateMatch[1]) : new Date();

        const cleanCurrency = (val) => {
            if (!val) return 0;
            val = String(val).replace(/[$",]/g, '');
            if (val.startsWith('(') && val.endsWith(')')) {
                val = '-' + val.substring(1, val.length - 1);
            }
            return parseFloat(val) || 0;
        };
        
        data.forEach(row => {
            const key = (row[0] || '').replace(/"/g, '').trim();
            const value = cleanCurrency(row[1]);
            salesData[key] = value;
        });

        const addTx = (desc, cat, amount) => {
            if (amount !== 0) {
                transactionsToInsert.push(applyCategorizationRules({
                    date: txDate,
                    description: desc,
                    amount: amount,
                    category: cat,
                    source: 'sales_summary_csv',
                }));
            }
        };
        
        addTx('Gross Sales', 'Income - Gross Sales', salesData['Gross sales'] || 0);
        addTx('Returns', 'Income - Returns', salesData['Returns'] || 0);
        addTx('Discounts & Comps', 'Expense - Discounts & Comps', salesData['Discounts & comps'] || 0);
        addTx('Taxes Collected', 'Liability - Sales Tax', salesData['Taxes'] || 0);
        addTx('Square Payment Processing Fees', 'Expense - Bank Fees', salesData['Square payment processing fees'] || 0);
    };

    // 2. Parse files
    try {
        if (req.files.bankFile) {
            const file = req.files.bankFile[0];
            const content = fs.readFileSync(file.path, 'utf8');
            const results = Papa.parse(content, { header: true, skipEmptyLines: true });
            processBankCSV(results.data);
            fs.unlinkSync(file.path); // Clean up
        }
        
        if (req.files.salesFile) {
            const file = req.files.salesFile[0];
            const content = fs.readFileSync(file.path, 'utf8');
            const results = Papa.parse(content, { header: false, skipEmptyLines: true });
            processSalesCSV(results.data, file.originalname);
            fs.unlinkSync(file.path); // Clean up
        }
    } catch (parseError) {
        console.error("File parse error:", parseError);
        return res.status(500).send('Error parsing files.');
    }

    // 3. Insert transactions into DB
    if (transactionsToInsert.length === 0) {
        return res.status(400).send('No valid transactions found to insert.');
    }

    const stmt = db.prepare('INSERT INTO transactions (userId, date, description, amount, category, source, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?)');
    db.serialize(() => {
        db.run('BEGIN TRANSACTION');
        transactionsToInsert.forEach(tx => {
            stmt.run(userId, tx.date.toISOString(), tx.description, tx.amount, tx.category, tx.source, new Date().toISOString());
        });
        db.run('COMMIT', (err) => {
            if (err) {
                console.error("DB commit error:", err);
                return res.status(500).send('Error saving transactions.');
            }
            stmt.finalize();
            res.status(201).send(`Successfully inserted ${transactionsToInsert.length} transactions.`);
        });
    });
});

// --- Serve Frontend ---
app.get('*', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

// --- Start Server ---
app.listen(port, () => {
    console.log(`SimpleLedger server running at http://localhost:${port}`);
});

