const passport = require('passport');
const OpenIDConnectStrategy = require('passport-openidconnect');

const express = require('express');
const { param } = require('./applications');
const router = express.Router();
import { Role } from '@app/shared/types/roles';
import pool from '../db';
import { requireLogin } from '../middleware/auth';
import { getUserRoles } from '../services/db/rolesService';
import { getUserState, mapDiscordtoID } from '../services/db/memberService';
import { MemberState } from '@app/shared/types/member';
import { toDateTime } from '@app/shared/utils/time';
import { logger } from '../services/logging/logger';
const querystring = require('querystring');
import { performance } from 'perf_hooks';
import { CacheService } from '../services/cache/cache';
import { Strategy as CustomStrategy } from 'passport-custom';


function parseJwt(token) {
    return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
}

const devLogin = (req: any, res: any, next: any) => {
    // The object here must match what your 'verify' function returns: { memberId }
    const devUser = { memberId: 1 }; // Hardcoded ID

    req.logIn(devUser, (err: any) => {
        if (err) return next(err);
        const redirectTo = req.session.redirectTo || process.env.CLIENT_URL;
        delete req.session.redirectTo;
        return res.redirect(redirectTo);
    });
};

if (process.env.AUTH_MODE === "mock") {
    passport.use('mock', new CustomStrategy(async (req, done) => {
        const mockUser = { memberId: 1 };
        return done(null, mockUser);
    }))
} else {
    passport.use('oidc', new OpenIDConnectStrategy({
        issuer: process.env.AUTH_ISSUER,
        authorizationURL: process.env.AUTH_DOMAIN + '/authorize/',
        tokenURL: process.env.AUTH_DOMAIN + '/token/',
        userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/',
        clientID: process.env.AUTH_CLIENT_ID,
        clientSecret: process.env.AUTH_CLIENT_SECRET,
        callbackURL: process.env.AUTH_REDIRECT_URI,
        scope: ['openid', 'profile', 'discord']
    }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) {

        // console.log('--- OIDC verify() called ---');
        // console.log('issuer:', issuer);
        // console.log('sub:', sub);
        // // console.log('discord:', discord);
        // console.log('profile:', profile);
        // console.log('jwt: ', parseJwt(jwtClaims));
        // console.log('params:', params);
        let con;

        try {
            con = await pool.getConnection();

            await con.beginTransaction();

            //lookup existing user
            const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]);
            let memberId: number | null = null;
            //if member exists
            if (existing.length > 0) {
                //login
                memberId = existing[0].id;
                logger.info('auth', `Existing member login`, {
                    memberId,
                    issuer,
                });

            } else {
                //otherwise: create account mode
                const jwt = parseJwt(jwtClaims);
                const discordID = jwt.discord?.id as number;

                //check if account is available to claim
                if (discordID)
                    memberId = await mapDiscordtoID(discordID);

                if (discordID && memberId) {
                    // claim account
                    const result = await con.query(
                        `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`,
                        [sub, issuer, memberId]
                    )
                    logger.info('auth', `Existing member claimed via Discord`, {
                        memberId,
                        discordID,
                        issuer,
                    });

                } else {
                    // new account
                    const username = sub.username;
                    const result = await con.query(
                        `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`,
                        [username, sub, issuer]
                    )
                    memberId = Number(result.insertId);

                    logger.info('auth', `New member account created`, {
                        memberId,
                        username,
                        issuer,
                    });

                }
            }

            await con.query(`UPDATE members SET last_login = ? WHERE id = ?`, [toDateTime(new Date()), memberId])

            await con.commit();
            return cb(null, { memberId });
        } catch (error) {
            logger.error('auth', `Authentication transaction failed`, {
                issuer,
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined,
            });

            if (con) {
                try {
                    await con.rollback();
                } catch (rollbackError) {
                    logger.error('auth', `Rollback failed`, {
                        error: rollbackError instanceof Error
                            ? rollbackError.message
                            : String(rollbackError),
                    });
                }
            }
            return cb(error);
        } finally {
            if (con) con.release();
        }
    }));
}

router.get('/login', (req, res, next) => {
    req.session.redirectTo = req.query.redirect as string;

    const strategy = process.env.AUTH_MODE === 'mock' ? 'mock' : 'oidc';

    passport.authenticate(strategy, {
        successRedirect: (req.session.redirectTo || process.env.CLIENT_URL),
        failureRedirect: '/login'
    })(req, res, next);
});

router.get('/callback', (req, res, next) => {

    //escape if mocked
    if (process.env.AUTH_MODE === 'mock') {
        return res.redirect(process.env.CLIENT_URL || '/');
    }

    const redirectURI = req.session.redirectTo;
    passport.authenticate('oidc', (err, user) => {
        if (err) return next(err);
        if (!user) return res.redirect(process.env.CLIENT_URL);

        req.logIn(user, err => {
            if (err) return next(err);

            // Use redirect saved from session
            const redirectTo = redirectURI || process.env.CLIENT_URL;
            delete req.session.redirectTo;
            return res.redirect(redirectTo);
        });
    })(req, res, next);
});

router.get('/logout', [requireLogin], function (req, res, next) {
    req.logout(function (err) {

        if (err) { return next(err); }

        req.session.destroy((err) => {
            if (err) { return next(err); }

            res.clearCookie('connect.sid', {
                path: '/',
                domain: process.env.CLIENT_DOMAIN,
                httpOnly: true,
                sameSite: 'lax'
            });

            if (process.env.AUTH_MODE === 'mock') {
                return res.redirect(process.env.CLIENT_URL || '/');
            }

            var params = {
                client_id: process.env.AUTH_CLIENT_ID,
                returnTo: process.env.CLIENT_URL
            };

            const endSessionUri = process.env.AUTH_END_SESSION_URI;
            if (endSessionUri) {
                return res.redirect(endSessionUri + '?' + querystring.stringify(params));
            } else {
                return res.redirect(process.env.CLIENT_URL || '/');
            }
        })
    });
});

passport.serializeUser(function (user, cb) {
    process.nextTick(function () {
        cb(null, user);
    });
});

passport.deserializeUser(function (user, cb) {
    const start = performance.now();
    const timings: Record<string, number> = {};

    process.nextTick(async function () {
        const memberID = user.memberId as number;
        let con;

        try {
            //cache lookup
            let t = performance.now();
            const cachedData: UserData | undefined = userCache.Get(memberID);
            timings.cache_lookup = performance.now() - t;

            if (cachedData) {
                timings.total = performance.now() - start;

                logger.info(
                    'profiling',
                    'passport.deserializeUser (cache hit)',
                    {
                        memberId: memberID,
                        cache_hit: true,
                        source: 'cache',
                        total_ms: timings.total,
                        breakdown_ms: timings,
                    },
                    'profiling'
                );

                return cb(null, cachedData);
            }

            //cache miss, db load
            t = performance.now();
            con = await pool.getConnection();
            timings.getConnection = performance.now() - t;

            t = performance.now();
            const userResults = await con.query(
                `SELECT id, name, discord_id FROM members WHERE id = ?;`,
                [memberID]
            );
            timings.memberQuery = performance.now() - t;

            const userData: UserData = userResults[0];

            t = performance.now();
            userData.roles = await getUserRoles(memberID) || [];
            timings.roles = performance.now() - t;

            t = performance.now();
            userData.state = await getUserState(memberID);
            timings.state = performance.now() - t;

            t = performance.now();
            userCache.Set(userData.id, userData);
            timings.cache_set = performance.now() - t;

            timings.total = performance.now() - start;

            logger.info(
                'profiling',
                'passport.deserializeUser (db load)',
                {
                    memberId: memberID,
                    cache_hit: false,
                    source: 'db',
                    total_ms: timings.total,
                    breakdown_ms: timings,
                },
                'profiling'
            );

            return cb(null, userData);

        } catch (error) {
            logger.error(
                'profiling',
                'passport.deserializeUser failed',
                {
                    memberId: memberID,
                    error: error instanceof Error ? error.message : String(error),
                }
            );
            return cb(error);
        } finally {
            if (con) con.release();
        }
    });
});

declare global {
    namespace Express {
        interface Request {
            user: {
                id: number;
                name: string;
                discord_id: string;
                roles: Role[];
                state: MemberState;
            };
        }
    }
}

export interface UserData {
    id: number;
    name: string;
    roles: Role[];
    state: MemberState;
    discord_id?: string;
}

const userCache = new CacheService<number, UserData>();

export const authRouter = router;
export const memberCache = userCache;