import bcrypt from 'bcrypt';
import fetch from 'cross-fetch';
import {PrismaClient, Role, UseCase} from '@prisma/client';
import requestIp from "request-ip";
import parser from "ua-parser-js";
import {NextApiRequest} from "next";
import {RestAPI} from "./stringExt";
import {Regrouped} from "../lib/environment";
import {regrouped} from "../lib/environment";
import {prisma} from "./utils";
import {Aggregate} from "./tmdb";

interface IP {
    status: string;
    country: string;
    countryCode: string;
    region: string;
    regionName: string;
    city: string;
    zip: string;
    lat: number;
    lon: number;
    timezone: string;
    isp: string;
    org: string;
    as: string;
    query: string;
}

export interface AuthInterface {
    error?: string
    response?: string
    payLoad?: {session: string, context: Role, email: string, validUntil: number, notificationChannel: string }
}

export interface ManageAuthKey {
    case: UseCase;
    name: string;
    key: string;
    backdrop: string;
    description: string;
    access: number;
}

export class Base extends RestAPI {
    protected readonly fetch: (input: RequestInfo, init?: (RequestInit | undefined)) => Promise<Response>;
    protected readonly prisma: PrismaClient;
    protected tmdb: Aggregate | null;
    protected readonly regrouped: Regrouped = regrouped;
    protected readonly socket: any;

    constructor() {
        super();
        this.prisma = prisma;
        this.fetch = fetch;
        if (this.regrouped.tmdbToken)
            this.tmdb = new Aggregate(this.regrouped.tmdbToken);
        else
            this.tmdb = null;
    }
}

export class Session extends Base {

    /**
     * @desc confirms that session /Session exists if account is guest, the account is deleted
     * @param session - session to check
     */
    public async validateSession(session: string): Promise<AuthInterface> {
        let result = await this.prisma.session.findFirst({where: {session}, include: {user: true}});
        if (result) {
            if (result.user.role === Role.GUEST) {
                try {
                    await this.prisma.user.delete({where: {userId: result.userId}});
                } catch (e) {
                    console.log(e);
                }

                return {error: 'guest session has expired'}
            }

            return result.valid > new Date() ? {
                response: 'valid',
                payLoad: {
                    session,
                    validUntil: new Date(result.valid).getTime(),
                    context: result.user.role,
                    email: result.user.email,
                    notificationChannel: result.user.notificationChannel
                }
            } : {error: 'app id has expired'};
        }

        return {error: 'invalid app id provided'};
    }

    /**
     * @desc creates a new session for a user
     * @param userId - user id to be stored with generated auth key
     */
    public async generateSession(userId: string): Promise<{session: string, validUntil: number}> {
        const session = this.generateKey(5, 5);
        let valid = new Date().getTime() + (7 * 24 * 60 * 60 * 1000);
        await this.prisma.session.create({
            data: {
                session: session, valid: new Date(valid),
                userId, created: new Date()
            }
        })
        return {session, validUntil: valid};
    }

    /**
     * @desc clears out all sessions for a specific user
     * @param session - session to be cleared
     * @param userId - user id to be cleared
     */
    public async clearSession(session: string, userId = false): Promise<boolean> {
        if (userId) {
            await this.prisma.userIdentifier.deleteMany({where: {userId: session}});
            const user = await this.prisma.session.deleteMany({where: {userId: session}});
            return user.count > 0;

        } else {
            let result = await this.prisma.session.findFirst({where: {session}});
            if (result) {
                await this.prisma.userIdentifier.deleteMany({where: {userId: result.userId}});
                const user = await this.prisma.session.deleteMany({where: {userId: result.userId}});
                return user.count > 0;
            }
        }

        return false;
    }

    /**
     * @desc deletes a specific session for a user
     * @param session - session to be deleted
     */
    public async clearSingleSession(session: string) {
        const state = await this.prisma.session.findUnique({where: {session}, include: {user: true}});
        if (state) {
            try {
                if (state.user.role === Role.GUEST)
                    await this.prisma.user.delete({where: {userId: state.userId}});
                else
                    await this.prisma.session.delete({where: {session}});
            } catch (e) {
                console.log(e)
            }
        }
    }

    /**
     * @desc saves the identity of a specific user's session
     * @param sessionId - session id to be stored
     * @param req - request object
     */
    public async saveIdentity(sessionId: string, req: NextApiRequest) {
        const address = requestIp.getClientIp(req);
        const user = await this.getUserFromSession(sessionId);
        const ua = parser(req.headers['user-agent']);
        if (address && address !== '127.0.0.1' && user) {
            const osName = ua.os.name;
            const userId = user.userId;
            const browserName = ua.browser.name + ' ' + ua.browser.version;
            const identity = await this.prisma.userIdentifier.findFirst({where: {address}});
            let country: string, regionName: string, city: string;

            if (identity) {
                city = identity.city;
                country = identity.country;
                regionName = identity.regionName;

            } else {
                const client = await this.makeRequest<IP>('http://ip-api.com/json/' + address, null, 'GET');
                city = client?.city || '';
                country = client?.country || '';
                regionName = client?.regionName || '';
            }

            const data = {osName: osName || '', userId, browserName, sessionId, address, regionName, country, city};
            if (city && country && regionName)
                await this.prisma.userIdentifier.upsert({
                    create: data,
                    update: data,
                    where: {sessionId}
                });
        }
    }

    /**
     * @desc gets a user from a session
     * @param sessionId - session id to be stored
     */
    public async getUserFromSession(sessionId: string) {
        const session = await this.prisma.session.findUnique({where: {session: sessionId}, include: {user: true}});
        if (session)
            return session.user;
        return null;
    }
}

export class Auth extends Session {

    /**
     * @desc verifies the role of the provided user
     * @param userId - user id to be verified
     */
    public async validateUser(userId: string): Promise<boolean> {
        const user = await this.prisma.user.findFirst({where: {userId}});
        return user ? user.role === Role.ADMIN : false;
    }

    /**
     * @desc generates an auth key if the user has the right to generate one
     * @param userId - user id to be verified
     */
    public async generateAuthKey(userId: string): Promise<string | null> {
        if (await this.validateUser(userId)) {
            const authKey = this.generateKey(4, 5);
            await this.prisma.auth.create({
                data: {
                    authKey,
                    userId, access: 0,
                    created: new Date()
                }
            })

            return authKey;
        }
        return null;
    }

    /**
     * @desc checks if the auth key is valid or even exists
     * @param authKey - auth key to be checked
     * @param context - context of the request
     */
    public async validateAuthKey(authKey: string, context: Role): Promise<number> {
        let value = -1;
        if (authKey === 'homeBase' && (context === Role.ADMIN || context === Role.GUEST))
            value = 0;

        const authFile = await this.prisma.auth.findUnique({where: {authKey}});
        if (authFile)
            value = authFile.access;

        return value;
    }

    /**
     * @desc clears out the auth key by assigning a user to it and updating the accessing
     * @param authKey - auth key to be cleared
     * @param userId - user id to be assigned
     * @param useCase - use case of the request
     * @param authView - auth view of the request
     */
    public async utiliseAuthKey(authKey: string, userId: string, useCase: UseCase, authView: string | null = null) {
        const auth = await this.prisma.auth.findUnique({where: {authKey}});
        const user = await this.prisma.user.findUnique({where: {userId}});
        if (user && auth && auth.access === 0)
            await this.prisma.auth.update({
                where: {authKey},
                data: {userId, access: auth.access + 1, auth: authView, useCase}
            })

        else if (authKey !== 'homeBase')
            throw new Error('Unauthorised access attempted');
    }

    /**
     * @desc provides information about all keys on the database
     * @param userId - user requesting the information
     */
    public async getKeys(userId: string): Promise<ManageAuthKey[]> {
        if (await this.validateUser(userId)) {
            const keys = await this.prisma.auth.findMany({
                include: {
                    user: true,
                    view: {include: {episode: true, video: {include: {media: true}}}}
                }, orderBy: {id: 'desc'}
            });

            const response: ManageAuthKey[] = [];

            for (let item of keys) {
                let description = '';
                let backdrop = '';

                if (item.access === 0)
                    description = item.user.email + ' created this auth key';

                else {
                    description = item.useCase === UseCase.SIGNUP ? item.user.email + ' signed up with this auth key' : '';

                    if (item.view) {
                        let media = item.view.video.media.name;
                        backdrop = item.view.video.media.backdrop;

                        if (item.view.episode)
                            media = media + `: S${item.view.episode.seasonId}, E${item.view.episode.episode}`;

                        description = item.user.email + ' downloaded ' + media + ' using this auth key';
                    }
                }

                response.push({
                    case: item.useCase,
                    description, backdrop, key: item.authKey,
                    name: 'Key: ' + item.authKey, access: item.access
                })
            }

            return response;
        }

        return []
    }
}

export default class User extends Auth {

    /**
     * @desc creates a new user with the given details
     * @param email - email of the user
     * @param password - password of the user
     * @param authKey - auth key of the user
     * @param role - role of the user
     * @returns Promise<AuthInterface> auth object on with either error or response on success
     */
    public async register(email: string, password: string, authKey: string, role?: Role): Promise<AuthInterface> {
        role = role || Role.USER;
        const confirmedEmail = role === Role.OAUTH || role === Role.GUEST;
        password = await bcrypt.hash(password, 10);
        const notificationChannel = this.generateKey(13, 7);
        let userId = this.createUUID();

        let user = await this.prisma.user.findFirst({where:{email}});

        if (user)
            return {error: 'this email already exists'};

        const validAuth = await this.validateAuthKey(authKey, role);
        if (validAuth !== 0) {
            const error = validAuth === -1 ? 'invalid auth key' : 'this auth key has already been used';
            return {error};
        }

        const userRes = await this.prisma.user.create({
            data: {email, password, userId, role, confirmedEmail, notificationChannel}
        });

        await this.utiliseAuthKey(authKey, userId, UseCase.SIGNUP);
        return !confirmedEmail ? {error: 'Please check your email for a verification link'}: {
            response: 'User created successfully',
            payLoad: {
                email: userRes.email,
                context: userRes.role,
                notificationChannel: userRes.notificationChannel,
                ...await this.generateSession(userRes.userId)
            }
        };
    }

    /**
     * @desc attempts to log in a user with the given credentials
     * @param email - email of the user
     * @param password - password of the user
     * @returns Promise<AuthInterface> auth object with payload on success just error
     */
    public async authenticateUser(email: string, password: string): Promise<AuthInterface> {
        let user = await this.prisma.user.findFirst({where: {email}})
        if (user)
            if (await bcrypt.compare(password, user.password))
                if (user.confirmedEmail)
                    return {
                        response: 'logged in',
                        payLoad: {
                            email: user.email,
                            context: user.role,
                            notificationChannel: user.notificationChannel,
                            ...await this.generateSession(user.userId)
                        }
                    };

                else
                    return {error: 'email not confirmed'};

            else
                return {error: 'Incorrect password'};

        else
            return {error: 'No such user exists'};
    }

    /**
     * @desc checks if the email exists for the react form
     * @param email - email of the user
     * @returns boolean !!user exists
     */
    public async validateEmail(email: string): Promise<boolean> {
        let user = await this.prisma.user.findFirst({where: {email}});
        return !!user;
    }

    /**
     * @desc attempts to modify a user's details with the given credentials
     * @param email - email of the user
     * @param password - password of the user
     * @param userId - userId of the user
     */
    public async modifyUser(email: string, password: string, userId: string) {
        password = await bcrypt.hash(password, 10);
        const user = await this.prisma.user.findUnique({where: {userId}});
        if (user) {
            if (user.role !== Role.OAUTH) {
                await this.clearSession(userId, true);
                await this.prisma.user.update({
                    data: {
                        email, password
                    }, where: {userId}
                })
            }
        }
    }

    /**
     * @desc creates a user from their OAUTH2 credentials
     * @param email - email of the user
     * @param password - password of the user
     * @param authKey - auth key of the user
     */
    public async oauthHandler(email: string, password: string, authKey: string): Promise<AuthInterface> {
        let response = await this.authenticateUser(email, `${password}`);
        if (response.error && response.error === 'No such user exists')
            response = await this.register(email, `${password}`, authKey, Role.OAUTH);

        return response;
    }

    /**
     * @desc creates the admin account and the guest accounts if they do not exist
     */
    public async createAccounts() {
        let password = this.generateKey(4, 5);
        password = await bcrypt.hash(password, 10);
        await this.prisma.user.upsert({
            create: {confirmedEmail: true, password, email: '[email protected]', role: Role.GUEST, userId: this.createUUID()},
            update: {},
            where: {email: '[email protected]'}
        });
        await this.prisma.user.upsert({
            where: {email: 'frames AI'},
            create: {confirmedEmail: true, password, email: 'frames AI', role: Role.ADMIN, userId: this.createUUID()},
            update: {}
        });
        if (this.regrouped.user)
            await this.prisma.user.upsert({
                create: {
                    confirmedEmail: true,
                    password: await bcrypt.hash(this.regrouped.user.admin_pass, 10),
                    email: this.regrouped.user.admin_mail,
                    role: Role.ADMIN,
                    userId: this.createUUID()
                },
                update: {},
                where: {email: this.regrouped.user.admin_mail}
            });
    }

    /**
     * @desc creates a guest user that in theory should be usable for one session
     * @param password - email of the user
     */
    public async createGuestUser(password: string) {
        const email = password + '@frames.local';
        return await this.register(email, password, 'homeBase', Role.GUEST);
    }

    /**
     * @desc changes the user's password and returns the new password in plain text
     * @param email - email of the user
     */
    public async forgotPassword(email: string): Promise<{password?: string, error?: string}> {
        const user = await this.prisma.user.findUnique({where: {email}});
        if (user) {
            const password = this.generateKey(4, 5);
            await this.prisma.user.update({
                data: {
                    password: await bcrypt.hash(password, 10)
                }, where: {userId: user.userId}
            });
            return {password};
        }

        return {error: 'No such user exists'};
    }
}