import { NextFunction, Request, Response } from 'express';
import xss from 'xss';
import TokenMgmt from './tokenMgmt';
import PasswordMgmt from './passwordMgmt';
import UserDatabaseService, {
    IUserListService,
} from '../../services/db/userList';
import { UserLoginInput, UserToken } from '../../declarations/validations';

// TODO set cookies to signedCookies

export default class SecurityUtil {
    private static accessTokenCookieName = 'accessToken';

    private static accessTokenRefreshPath = '/auth';

    private static accessTokenValidityPeriodInSeconds = 1800;

    private static refreshTokenCookieName = 'refreshToken';

    private static refreshTokenValidityPeriodInDays = 30;

    public static userListService: IUserListService = new UserDatabaseService();

    static initialize() {
        TokenMgmt.accessTokenValidityPeriodInSeconds =
            SecurityUtil.accessTokenValidityPeriodInSeconds;
        TokenMgmt.refreshTokenValidityPeriodInDays =
            SecurityUtil.refreshTokenValidityPeriodInDays;
    }

    static currentUser(req: Request) {
        const result = UserToken.safeParse({
            ...req.user,
        });
        return result.success ? result.data : undefined;
    }

    static currentRole(req: Request) {
        const currentUser = SecurityUtil.currentUser(req);
        return currentUser?.role;
    }

    static isLoggedIn(req: Request) {
        return !!SecurityUtil.currentUser(req);
    }

    static handleAuthorizeRoleMiddleware(requiredRole: string) {
        return (req: Request, res: Response, next: NextFunction) => {
            const currentRole = SecurityUtil.currentRole(req);

            if (!currentRole || currentRole !== requiredRole) {
                res.sendStatus(403);
                return;
            }

            next();
        };
    }

    static sanitizeXssMiddleware() {
        return (req: Request, res: Response, next: NextFunction) => {
            for (const key of Object.keys(req.body)) {
                if (typeof req.body[key] !== 'string') {
                    continue;
                }

                if (key.includes('password')) {
                    continue;
                }

                req.body[key] = xss(req.body[key]);
            }

            next();
        };
    }

    static handleAuthenticateMiddleware() {
        return (req: Request, res: Response, next: NextFunction) => {
            if (SecurityUtil.isLoggedIn(req)) next();

            //const accessToken = req.cookies[this.accessTokenCookieName];
            //const { accessToken } = req.body;
            const accessToken = this.getAuthorizationHeaderToken(req);

            if (!accessToken) {
                SecurityUtil.handleMissingToken(res);
                return;
            }

            const response = TokenMgmt.validateAccessToken(accessToken);

            if (response.error) {
                SecurityUtil.handleUnauthorizedToken(res);
                return;
            }

            const result = UserToken.safeParse(response.user);
            if (!result.success) {
                SecurityUtil.handleUnauthorizedToken(res);
                return;
            }

            req.user = result.data;
            next();
        };
    }

    private static getAuthorizationHeaderToken(
        req: Request
    ): string | undefined {
        const bearerHeader = req.headers.authorization;

        if (!bearerHeader) {
            return '';
        }

        const bearer = bearerHeader.split(' ');
        const bearerToken = bearer[1];
        return bearerToken;
    }

    static async refreshSession(req: Request, res: Response) {
        //const refreshToken = req.cookies[this.refreshTokenCookieName];
        const refreshToken = this.getAuthorizationHeaderToken(req);

        if (!refreshToken) {
            this.handleMissingToken(res);
            return;
        }

        TokenMgmt.refreshAccessToken(refreshToken)
            .then((accessToken) => {
                this.addAccessTokenCookie(res, accessToken);
                res.send({ success: 'Successfully refreshed', accessToken });
            })
            .catch(() => this.handleUnauthorizedToken(res));
    }

    static async handleLogin(req: Request, res: Response) {
        const result = UserLoginInput.safeParse(req.body);
        if (!result.success) {
            this.handleMissingProp(res);
            return;
        }

        const { username, password, role } = result.data;

        const dbUser = await this.userListService.findUserByUsername(username);

        if (!dbUser) {
            this.handleLoginMissedAttempt(res);
            return;
        }

        const {
            passwordHash,
        } = (await this.userListService.findPasswordHashByUserId(dbUser.id))!;

        if (!(await PasswordMgmt.verifyPassword(password, passwordHash))) {
            this.handleLoginMissedAttempt(res);
            return;
        }

        const selectedUserRole = dbUser.roles.find(
            (dbRole: any) => dbRole.name === role
        );
        if (!selectedUserRole) {
            this.handleLoginMissedAttempt(res);
            return;
        }

        const loginTimestamp = new Date();

        const user = {
            id: dbUser.id,
            username: dbUser.username,
            lastLogin: loginTimestamp,
            role: selectedUserRole.name,
            profileId: dbUser.profileId, // TODO update to single table -> rm single profile per user 1-1 relation
        };

        const accessToken = TokenMgmt.generateAccessToken(user);
        const refreshToken = TokenMgmt.generateRefreshToken(user);
        const expireAt = new Date();
        expireAt.setDate(
            expireAt.getDate() + this.refreshTokenValidityPeriodInDays
        );

        this.userListService
            .openSession(user.id, refreshToken, loginTimestamp, expireAt)
            .then(() => {
                this.addAccessTokenCookie(res, accessToken);
                this.addRefreshTokenCookie(res, refreshToken, expireAt);
                res.send({
                    success: 'Logged in successfully!',
                    accessToken,
                    refreshToken,
                    user,
                });
            })
            .catch(() => this.handleInternalServerError(res));
    }

    static handleLogout(req: Request, res: Response) {
        //const refreshToken = req.cookies[this.refreshTokenCookieName];
        const refreshToken = this.getAuthorizationHeaderToken(req);
        res.clearCookie(this.accessTokenCookieName);
        res.clearCookie(this.refreshTokenCookieName, {
            path: this.accessTokenRefreshPath,
        });

        if (!refreshToken) {
            res.send({ success: 'logged out successfully' });
            return;
        }

        this.userListService.closeSession(refreshToken).finally(() => {
            res.send({ success: 'logged out successfully' });
        });
    }

    private static addAccessTokenCookie(res: Response, token: string) {
        res.cookie(this.accessTokenCookieName, token, {
            httpOnly: true,
            maxAge: this.accessTokenValidityPeriodInSeconds * 1000,
            // TODO secure: true, with https
            sameSite: 'strict',
        });
    }

    private static addRefreshTokenCookie(
        res: Response,
        token: string,
        expireAt: Date
    ) {
        res.cookie(this.refreshTokenCookieName, token, {
            httpOnly: true,
            expires: expireAt,
            path: this.accessTokenRefreshPath,
            // TODO secure: true, with https
            sameSite: 'strict',
        });
    }

    private static handleInternalServerError(res: Response) {
        res.status(500).send({
            error: 'Internal Server Error. Please try again later',
        });
    }

    private static handleLoginMissedAttempt(res: Response) {
        res.status(401)
            .header(
                'WWW-Authenticate',
                'Bearer realm="Access to the staging site", charset="UTF-8"'
            )
            .send({ error: 'Incorrect username or password' });
    }

    private static handleMissingToken(res: Response) {
        res.status(401)
            .header(
                'WWW-Authenticate',
                'Bearer realm="Access to the staging site", charset="UTF-8"'
            )
            .send({ error: 'Required Token not found' });
    }

    private static handleUnauthorizedToken(res: Response, reason?: any) {
        res.status(401)
            .header(
                'WWW-Authenticate',
                `Bearer realm="Access to the staging site", charset="UTF-8"`
            )
            .send({
                error: 'Used Token is expired or invalid',
                reason,
            });
    }

    private static handleMissingProp(res: Response) {
        res.status(400).send({
            error: 'A required prop is missing',
        });
    }
}
SecurityUtil.initialize();
