import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import pool from '../db.js';
import { sendEmail } from './email.service.js';

/** 
 * Generiert einen zufälligen 6-stelligen Code als String 
 */
const generateOTP = () => {
    return Math.floor(100000 + Math.random() * 900000).toString();
};

/**
 * Registriert einen neuen Nutzer, speichert das OTP und löst den E-Mail-Versand aus.
 * 
 * @param {string} username     - Der gewünschte Benutzername
 * @param {string} email        - Die E-Mail-Addresse
 * @param {string} password     - Das Klartext-Passwort
 * @returns {object}            - Die accountId des neuen Nutzers
 */
export const registerUser = async (username, email, password) => {
    // Wir holen uns einen dedizierten Client aus dem pool für unsere Transaktion 
    const client = await pool.connect();

    const lowerCaseEmail = email.toLowerCase();

    try {
        // Transaktion starten: Entweder alles klappt, oder nichts wird gespeichert
        await client.query('BEGIN');

        // Prüfen, ob der Nutzer oder die E-Mail schon existiert
        const checkResult = await client.query(`
            SELECT username, email FROM lobby_accounts 
            WHERE LOWER(username) = LOWER($1) OR email = $2`,
            [username, lowerCaseEmail]
        );

        if (checkResult.rows.length > 0) {
            const existingRecord = checkResult.rows[0];

            if (existingRecord.email === lowerCaseEmail) {
                const error = new Error('Email already exists');
                error.code = 'ERR_EMAIL_TAKEN';
                throw error;
            } else {
                const error = new Error('Username already exists');
                error.code = 'ERR_USERNAME_TAKEN';
                throw error;
            }
        }

        // Passwort sicher hashen
        const passwordHash = await argon2.hash(password);

        // Nutzer in de Datenbank eintragen
        const insertAccount = await client.query(`
            INSERT INTO lobby_accounts (username, email, password_hash)
            VALUES ($1, $2, $3) RETURNING id`,
            [username, lowerCaseEmail, passwordHash]
        );
        const accountId = insertAccount.rows[0].id;

        // OTP generieren uns in der auth_otps Tabelle speichern (15min Gültigkeit)
        const otpCode = generateOTP();
        await client.query(`
            INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
            VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
            [accountId, otpCode]
        );

        // Registierungs E-Mail über unseren E-Mail Service versenden
        const mailHtml = `
            <h2>Willkommen bei Void-Genesis, ${username}!</h2>
            <p>Dein Verifizierungscode lautet:</p>
            <h1 style="color: #00f0ff; letter-spacing: 5px;">${otpCode}</h1>
            <p>Dieser Code ist 15 Minuten lang gültig.</p>
        `;
        await sendEmail(lowerCaseEmail, username, 'Void-Genesis - Aktiviere deinen Account', mailHtml);

        // Wenn wir hier ankommen, lief alles fehlerfrei, wir committen die Daten.
        await client.query('COMMIT');

        return { accountId };
    } catch (error) {
        // bei JEDEM Fehler machen wir einen Rollback. Die Datenbank bleibt sauber.
        await client.query('ROLLBACK');

        if (error.code === '23505') {
            if (error.constraint === 'lobby_accounts_username_key') {
                const customErr = new Error('Username already exists');
                customErr.code = 'ERR_USERNAME_TAKEN';
                throw customErr;
            }
            if (error.constraint === 'lobby_accounts_email_key') {
                const customErr = new Error('Email already exists');
                customErr.code = 'ERR_EMAIL_TAKEN';
                throw customErr;
            }
        }

        throw error;
    } finally {
        client.release();
    }
}

/**
 * Verifiziert die E-Mail-Adresse eines Nutzers
 * 
 * @param {string} email    - Die E-Mail-Addresse des Nutzers
 * @param {string} otpCode  - Der 6-stellige Code aus der E-Mail
 * @returns {boolean}       - true wenn erfolgreich
 */
export const verifyEmail = async (email, otpCode) => {
    const client = await pool.connect();
    const lowerCaseEmail = email.toLowerCase();

    try {
        await client.query('BEGIN');

        // Nutzer anhand der E-Mail suchen un die ID sowie Verifizierungsstatus abfragen
        const accountResult = await client.query(
            'SELECT id, is_email_verified FROM lobby_accounts WHERE email = $1',
            [lowerCaseEmail]
        );

        if (accountResult.rows.length === 0) {
            // ERR_USER_NOT_FOUND: Es existiert kein Account mit dieser E-Mail
            const error = new Error('User not found');
            error.code = 'ERR_USER_NOT_FOUND';
            throw error;
        }

        const account = accountResult.rows[0];

        // Prüfen, ob der Nutzer nicht ohnehin schon verifiziert ist
        if (account.is_email_verified) {
            // ERR_ALREADY_VERIFIED: Diese E-Mail-Addresse wurde bereits bestätigt
            const error = new Error('Already verified');
            error.code = 'ERR_ALREADY_VERIFIED';
            throw error;
        }

        // Den passenden OTP-Code in der Datenbank suchen
        const otpResult = await client.query(`
            SELECT id, expires_at FROM auth_otps
            WHERE account_id = $1 AND otp_code = $2 AND type = 'register'`,
            [account.id, otpCode]
        );

        if (otpResult.rows.length === 0) {
            // ERR_INVALID_OTP: Der eingegebene Code ist falsch.
            const error = new Error('Invalid OTP');
            error.code = 'ERR_INVALID_OTP';
            throw error;
        }

        const otpRecord = otpResult.rows[0];

        // Prüfen, ob der Cide bereits abgelaufen ist (älter als 15 Minuten)
        if (new Date() > new Date(otpRecord.expires_at)) {
            // ERR_OTP_EXPIRED: Der Code ist abgelaufen. Bitte fordere einen neuen an.
            const error = new Error('OTP expired');
            error.code = 'ERR_OTP_EXPIRED';
            throw error;
        }

        // Account als verifiziert markieren
        await client.query(
            'UPDATE lobby_accounts SET is_email_verified = true WHERE id = $1',
            [account.id]
        );

        // Den genutzten OTP-Code aus der Datenbank löschen (Sicherheit und Sauberkeit)
        await client.query(
            'DELETE FROM auth_otps WHERE id = $1',
            [otpRecord.id]
        );

        // Alles lief perfekt, Änderungen speichern
        await client.query('COMMIT');
        return true;

    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    } finally {
        client.release();
    }
};

/**
 * Loggt einen Nutzer ein, verifiziert das Passwort uns erstellt JWT-Tokens
 * 
 * @param {string} identifier   - Kann der Benutzername oder die E-Mail-Address sein
 * @param {string} password     - Das eingegebene Klartext-Passwort
 * @returns {object}            - Enthält accessToken, refreshToken und grundlegende Nutzerdaten
 */
export const loginUser = async (identifier, password) => {
    const client = await pool.connect();

    try {
        // Nutzer suchen
        const accountResult = await client.query(`
            SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
            FROM lobby_accounts
            WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
            [identifier]
        );

        // Wenn kein Nutzer gefunden wurde
        if (accountResult.rows.length === 0) {
            // ERR_INVALID_CREDENTIALS: Benutzername/E-Mail oder Passwort falsch
            const error = new Error('Invalid credentials');
            error.code = 'ERR_INVALID_CREDENTIALS';
            throw error;
        }

        const account = accountResult.rows[0];

        // Prüfen, ob die Email bereits bestätigt wurde
        if (!account.is_email_verified) {
            // ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
            const error = new Error('Email not verified');
            error.code = 'ERR_EMAIL_NOT_VERIFIED';
            error.email = account.email;
            throw error;
        }

        // Passwort vergleichen (argon2)
        const isPasswordValid = await argon2.verify(account.password_hash, password);
        if (!isPasswordValid) {
            const error = new Error('Invalied credentials');
            error.code = 'ERR_INVALID_CREDENTIALS';
            throw error;
        }

        // JWT-Tokens geneieren
        // Das Access-Token speichert die wichtigsten Infos für schnelle Abfragen
        const tokenPayload = {
            accountId: account.id,
            username: account.username,
            isAdmin: account.is_admin
        };

        const accessToken = jwt.sign(
            tokenPayload,
            process.env.JWT_ACCESS_SECRET,
            { expiresIn: '15m' }
        );

        const refreshToken = jwt.sign(
            { accountId: account.id },
            process.env.JWT_REFRESH_SECRET,
            { expiresIn: '30d' }
        );

        // Refresh-Token in der Datenbank speichern (Vorbereitung für das Single-Session System)
        await client.query(`
            INSERT INTO refresh_tokens (account_id, token, expires_at)
            VALUES ($1, $2, NOW() + INTERVAL '30 days')`,
            [account.id, refreshToken]
        );

        // Erfolgreiche Rückgabe der Daten
        return {
            accessToken,
            refreshToken,
            user: {
                id: account.id,
                username: account.username,
                email: account.email,
                res_premium: account.res_premium
            }
        };
    } finally {
        client.release();
    }
};

/**
 * Prüft ein Refresh-Token und generiert ein neues, aktuelles Access-Token
 * 
 * @param {string} refreshToken - Das langlebige Refresh-Token des Nutzers.
 * @returns {object}            - Enthält das neue accessToken
 */
export const refreshAccessToken = async (refreshToken) => {
    const client = await pool.connect();

    try {
        // JWT-Signatur und Ablaufdatum des Refresh-Tokens kryptografisch verifizieren
        // Wenn das Token manipuliert oder abgelaufen ist, wirft jwt.verify sofort einen Fehler.
        const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
        const { accountId } = payload;

        // Sicherheits-Check: Existiert diese Token noch in unserer Datenbank?
        // Wenn sich ein zweites Gerät eingeloggt hat, wurden die altenm Tokens gelöscht.
        // Dieser Check schlägt dasnn fehl und blockiert den unerlaubten Zugriff!
        const tokenResult = await client.query(
            'SELECT id FROM refresh_tokens WHERE account_id = $1 AND token = $2',
            [accountId, refreshToken]
        );

        if (tokenResult.rows.length === 0) {
            const error = new Error('Refresh token was revoked or does not exist');
            error.code = 'ERR_UNAUTHORIZED';
            throw error;
        }

        // Aktuelle Benutzerdaten frosch aus der Datenbank abfragen
        // Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde, 
        // steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
        const accountResult = await client.query(
            'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
            [accountId]
        );

        if (accountResult.rows.length === 0) {
            const error = new Error('Account no longer exists');
            error.code = 'ERR_UNAUTHORIZED';
            throw error;
        }

        const account = accountResult.rows[0];

        // Neues kurzlebiges Access-Token erstellen (wieder für 15 min)
        const tokenPayload = {
            accountId: account.id,
            username: account.username,
            isAdmin: account.is_admin
        };

        const accessToken = jwt.sign(
            tokenPayload,
            process.env.JWT_ACCESS_SECRET,
            { expiresIn: '15m' }
        );

        return {
            accessToken,
            user: {
                id: account.id,
                username: account.username,
                email: account.email,
                res_premium: account.res_premium
            }
        };
    } catch (error) {
        // Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
        if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
            const customErr = new Error('Invalid, expired or malformed token');
            customErr.code = 'ERR_UNAUTHORIZED';
            throw customErr;
        }

        // Alle anderen Fehhler (zB Datenbank Fehler) weiterwerfen
        throw error;
    } finally {
        client.release();
    }
}

/**
 * Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
 * Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
 * 
 * @param {string} email - Die E-Mail Addresse des Nutzers
 * @returns {boolean}    - true, wenn die E-Mail erfolgreich versendet wurde.
 */
export const resendOTP = async (email) => {
    const client = await pool.connect();
    const lowerCaseEmail = email.toLowerCase();

    try {
        await client.query('BEGIN');

        // Prüfen, ob der Nutzer überhaupt existiert
        const accountResult = await client.query(
            'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
            [lowerCaseEmail]
        );

        if (accountResult.rows.length === 0) {
            // Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
            // dass die E-Mail nicht existieren, aber für unser internes Error-Handling
            // werfen wir einen spezifischen Fehler.
            const error = new Error('User nor found');
            error.code = 'ERR:USER_NOT_FOUND';
            throw error;
        }

        const account = accountResult.rows[0];

        // Prüfen, ob der Account nicht vielleicht schon verifiziert ist
        // Wenn ja, macht ein neuer Code keinen Sinn.
        if (account.is_email_verified) {
            const error = new Error('Already verified');
            error.code = 'ERR_ALREADY_VERIFIED';
            throw error;
        }

        const lastOtpResult = await client.query(`
            SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
            [account.id]
        );

        if (lastOtpResult.rows.length > 0) {
            const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
            const currentTime = new Date().getTime();
            const timeDifference = currentTime - lastCreatedTime;

            if (timeDifference < 120000) {
                const error = new Error('Cooldown active');
                error.code = 'ERR_COOLDOWN_ACTIVE';
                throw error;
            }
        }

        // Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
        // So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
        // Spam-Risiken.
        await client.query(
            `DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
            [account.id]
        );

        // Einen frischen, 6-stelligen Cide generieren
        const newOtpCode = generateOTP();

        // Den neuen Code in die Datenbank eintragen
        await client.query(`
            INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
            VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
            [account.id, newOtpCode]
        );

        // 6. Die neue E-Mail zusammenbauen und versenden
        const mailHtml = `
            <h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
            <p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
            <h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
            <p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
        `;

        await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);

        await client.query('COMMIT');
        return true;

    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    } finally {
        client.release();
    }
};

/**
 * Startet den Prozess zum Zurücksetzten des Passworts.
 * Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
 * 
 * @param {string} email - Die Email-Adresse des Nutzers
 * @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
 */
export const requestPasswordReset = async (email) => {
    const client = await pool.connect();
    const lowerCaseEmail = email.toLowerCase();

    try {
        await client.query('BEGIN');

        const accountResult = await client.query(
            'SELECT id, username FROM lobby_accounts WHERE email = $1',
            [lowerCaseEmail]
        );

        if (accountResult.rows.length === 0) {
            const error = new Error('User not found');
            error.code = 'ERR_USER_NOT_FOUND';
            throw error;
        }

        const lastOtpResult = await client.query(`
            SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
            [accountResult.id]
        );

        if (lastOtpResult.rows.length > 0) {
            const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
            const currentTime = new Date().getTime();
            const timeDifference = currentTime - lastCreatedTime;

            if (timeDifference < 120000) {
                const error = new Error('Cooldown active');
                error.code = 'ERR_COOLDOWN_ACTIVE';
                throw error;
            }
        }

        const account = accountResult.rows[0];

        await client.query(
            `DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
            [account.id]
        );

        const resetCode = generateOTP();

        await client.query(`
            INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
            VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`,
            [account.id, resetCode]
        );

        const mailHtml = `
            <h2>Hallo ${account.username},</h2>
            <p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
            <p>Dein Code lautet:</p>
            <h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
            <p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
        `;

        await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);

        await client.query('COMMIT');
        return true;

    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    } finally {
        client.release();
    }
};


/**
 * Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
 * 
 * @param {string} email - Die E-Mail Addresse des Nutzers
 * @param {string} otpCode - Der 6-stellige Code aus der E-Mail
 * @param {string} newPassword - das neue Klartext passwort
 * @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
 */
export const resetPassword = async (email, otpCode, newPassword) => {
    const client = await pool.connect();
    const lowerCaseEmail = email.toLowerCase();

    try {
        await client.query('BEGIN');
        
        const accountResult = await client.query(
            'SELECT id FROM lobby_accounts WHERE email = $1',
            [lowerCaseEmail]
        );

        if (accountResult.rows.length === 0) {
            const error = new Error('User not found');
            error.code = 'ERR_USER_NOT_FOUND';
            throw error;
        }

        const account = accountResult.rows[0];

        const otpResult = await client.query(`
            SELECT id, expires_at FROM auth_otps
            WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`,
            [account.id, otpCode]
        );

        if (otpResult.rows.length === 0) {
            const error = new Error('Invalid OTP');
            error.code = 'ERR_INVALID_OTP';
            throw error;
        }

        const otpRecord = otpResult.rows[0];

        if (new Date() > new Date(otpRecord.expires_at)) {
            const error = new Error('OTP expired');
            error.code = 'ERR_OTP_EXPIRED';
            throw error;
        }

        const passwordHash = await argon2.hash(newPassword);

        await client.query(
            'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
            [passwordHash, account.id]
        );

        await client.query(
            'DELETE FROM refresh_tokens WHERE account_id = $1',
            [account.id]
        );

        await client.query('COMMIT');
        return true;

    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    } finally {
        client.release();
    }
};
