import prisma from "@/prisma";
import { cookies } from "next/dist/client/components/headers";
import { v4 } from "uuid";
import * as ldap from "../ldap";
import { getLogger } from "../logger";
import { AuthTicket, DiscordAuth, User } from "@prisma/client";
import { DiscordAuthProvider } from "./discord";
import { GitHubAuthProvider } from "./github";
import { AuthProvider } from "./AuthProvider";

const logger = getLogger("auth.ts");

export class GluestickUser {
  private readonly dbTicket: AuthTicket;
  private dbUser: User;

  username: string | null;

  constructor(ticket: AuthTicket, user: User) {
    this.dbTicket = ticket;
    this.dbUser = user;

    this.username = user?.username;
  }

  get id(): number {
    return this.dbUser.id;
  }

  get isRegistering(): boolean {
    return this.username == null;
  }

  get authTicket(): string {
    return this.dbTicket.ticket;
  }

  async updateUsername(username?: string) {
    const user = await prisma.user.update({
      where: {
        id: this.dbUser.id
      },
      data: {
        username
      }
    });

    this.dbUser = user;
    this.username = username ?? null;
  }

  async getDiscord(): Promise<DiscordAuthProvider | null> {
    const discord = await prisma.discordAuth.findFirst({
      where: {
        userId: this.dbUser.id
      }
    });

    return discord === null
      ? null
      : new DiscordAuthProvider(discord.accessToken);
  }

  async getGitHub(): Promise<GitHubAuthProvider | null> {
    const github = await prisma.gitHubAuth.findFirst({
      where: {
        userId: this.dbUser.id
      }
    });

    return github === null ? null : new GitHubAuthProvider(github.accessToken);
  }

  async getAuthProviders(): Promise<AuthProvider[]> {
    const providers = [];

    const discord = await this.getDiscord();
    if (discord !== null) providers.push(discord);

    const github = await this.getGitHub();
    if (github !== null) providers.push(github);

    return providers;
  }
}

async function getAuthTicket() {
  const cookieStore = cookies();
  const cookieTicket = cookieStore.get("ticket");
  if (cookieTicket == null) return null;

  const ticket = await prisma.authTicket.findFirst({
    where: {
      ticket: cookieTicket.value
    }
  });
  return ticket ?? null;
}

export async function getRegisteringUser(ticket?: AuthTicket) {
  if (ticket == null) {
    let newTicket = await getAuthTicket();
    if (newTicket == null) return null;
    ticket = newTicket;
  }

  const user = await prisma.user.findFirst({
    where: {
      id: ticket.userId
    },
    include: {
      discordAuth: true,
      githubAuth: true
    }
  });

  return user;
}

export async function getUser(ticket?: AuthTicket) {
  if (ticket == null) {
    let newTicket = await getAuthTicket();
    if (newTicket == null) return null;
    ticket = newTicket;
  }

  const user = await prisma.user.findFirst({
    where: {
      id: ticket.userId
    }
  });

  if (
    user !== null &&
    user.username !== null &&
    !(await ldap.checkUserExists(user.username))
  ) {
    logger.warn(
      { username: user.username },
      "user doesn't exist in ldap anymore"
    );

    user.username = null;
    await prisma.user.update({
      where: {
        id: user.id
      },
      data: {
        username: null
      }
    });
  }

  if (user === null) return null;

  return new GluestickUser(ticket, user);
}

export enum AuthState {
  LoggedOut, // no auth token
  Registering, // has an auth token but no LDAP
  LoggedIn // has an auth token and LDAP
}

export async function getAuthState() {
  const ticket = await getAuthTicket();
  if (ticket == null) return AuthState.LoggedOut;

  const user = await getUser(ticket);
  if (user == null || user.isRegistering) return AuthState.Registering;

  return AuthState.LoggedIn;
}

export async function getCode(request: Request) {
  let url = new URL(request.url);
  let code = url.searchParams.get("code");
  let state = url.searchParams.get("state");

  if (code === null || state === null) {
    logger.info("request made with missing code/state");
    return new Response("missing code/state", { status: 400 });
  }

  const cookieStore = cookies();
  let cookieState = cookieStore.get("state");
  // prevent forgery
  if (cookieState?.value !== state) {
    logger.info(
      "request made with invalid state - someone attempting forgery?"
    );
    return new Response("state is invalid", { status: 400 });
  }

  return code;
}

export async function authTicketOAuth(id: number): Promise<[User, AuthTicket]> {
  // Handle the case in which we already have a user
  // (clicked on oauth button with existing ticket)
  const user = (await prisma.user.findFirst({
    where: {
      id: id
    }
  }))!;

  // check if user got deleted from ldap, same as /api/register
  if (user.username !== null && !(await ldap.checkUserExists(user.username))) {
    logger.warn(
      { username: user.username },
      "user doesn't exist in ldap anymore"
    );
    user.username = null;
    await prisma.user.update({
      where: {
        id: user.id
      },
      data: {
        username: null
      }
    });
  }

  const authTicket = await prisma.authTicket.upsert({
    where: {
      userId: user!.id
    },
    create: {
      userId: user!.id,
      ticket: v4(),
      expiresAt: new Date(Date.now() + 86400000)
    },
    update: {
      ticket: v4(),
      expiresAt: new Date(Date.now() + 86400000)
    }
  });

  await prisma.user.update({
    where: {
      id: user!.id
    },
    data: {
      authTicket: {
        connect: {
          id: authTicket.id
        }
      }
    }
  });

  return [user, authTicket];
}

export async function authTicketLogin(
  username: string
): Promise<[User, AuthTicket]> {
  let user = await prisma.user.findFirst({
    where: {
      username
    }
  });

  // maybe our DB got reset? let's just create a new user
  if (user === null) {
    logger.warn({ username }, "user exists in ldap, but not database");
    user = await prisma.user.create({
      data: {
        username
      }
    });
  }

  const authTicket = await prisma.authTicket.upsert({
    where: {
      userId: user!.id
    },
    create: {
      userId: user!.id,
      ticket: v4(),
      expiresAt: new Date(Date.now() + 86400000)
    },
    update: {
      ticket: v4(),
      expiresAt: new Date(Date.now() + 86400000)
    }
  });

  await prisma.user.update({
    where: {
      id: user!.id
    },
    data: {
      authTicket: {
        connect: {
          id: authTicket.id
        }
      }
    }
  });

  return [user, authTicket];
}
