import { ApolloClient, InMemoryCache } from "@apollo/client";
import { Client } from "ldapts";
import { gql } from "./__generated__";
import { BerWriter } from "asn1";
import { User } from "@prisma/client";
import { ensureJpg } from "@/image";
import prisma from "./prisma";

export type LLDAPAuthResponse = {
  token: string;
  refreshToken: string;
};

type LLDAPRefreshResponse = {
  token: string;
};

export type UserInfo = {
  username: string;
  displayName: string;
  email: string;
  avatar?: string;

  discordId?: string;
  githubId?: string;
};

async function getLdapClient() {
  if (global.ldapClient == null) {
    global.ldapClient = new Client({
      url: `ldap://${process.env.LDAP_HOST}:3890`
    });

    const full = `uid=${process.env.LDAP_BIND_USER},ou=people,${process.env.LDAP_DC}`;
    await global.ldapClient.bind(full, process.env.LDAP_BIND_PASSWORD);
  }

  return global.ldapClient;
}

async function regenAuthToken() {
  if (global.authResponse != null) {
    const url = `http://${process.env.LDAP_HOST}:17170/auth/refresh`;
    const req = await fetch(url, {
      headers: {
        "Refresh-Token": global.authResponse.refreshToken
      }
    });
    const res: LLDAPRefreshResponse = await req.json();
    global.authResponse.token = res.token;
  } else {
    const url = `http://${process.env.LDAP_HOST}:17170/auth/simple/login`;
    const req = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        username: process.env.LDAP_BIND_USER,
        password: process.env.LDAP_BIND_PASSWORD
      })
    });

    global.authResponse = await req.json();
  }

  // valid for one day, so refresh every 12 hours
  setTimeout(regenAuthToken, 12 * 60 * 60 * 1000);
}

async function getAuthToken() {
  if (global.authResponse == null) await regenAuthToken();
  return global.authResponse!.token;
}

async function getGraphQLClient() {
  if (global.authResponse == null) {
    await getAuthToken();
  }

  // Remake the client every time because Apollo caching is fucking stupid
  let graphQLClient = new ApolloClient({
    uri: `http://${process.env.LDAP_HOST}:17170/api/graphql`,
    cache: new InMemoryCache(),
    defaultOptions: {
      watchQuery: {
        fetchPolicy: "no-cache",
        errorPolicy: "ignore"
      },
      query: {
        fetchPolicy: "no-cache",
        errorPolicy: "all"
      }
    },
    headers: {
      Authorization: `Bearer ${global.authResponse!.token}`
    }
  });

  // whoever designed to cache this shit is FUCKING STUPID
  return graphQLClient;
}

export async function getUsers() {
  const client = await getGraphQLClient();
  const query = await client.query({
    query: gql(`
      query GetUsers {
         users {
          id
        }
      }
    `)
  });

  return query.data.users;
}

export async function createUser(
  username: string,
  displayName: string,
  email: string,
  avatar?: Buffer
) {
  const fixedAvatar = avatar != null ? await ensureJpg(avatar) : null;

  const client = await getGraphQLClient();
  const mutation = await client.mutate({
    mutation: gql(`
      mutation CreateUser($user: CreateUserInput!) {
        createUser(user: $user) {
          id
        }
      }
    `),
    variables: {
      user: {
        id: username,
        displayName,
        email,
        avatar: fixedAvatar
      }
    }
  });
}

export async function setPassword(user: string, password: string) {
  const client = await getLdapClient();
  const dn = `uid=${user},ou=people,${process.env.LDAP_DC}`;

  // god bless random stackoverflow user
  // https://stackoverflow.com/questions/65745679/how-do-i-pass-parameters-to-the-ldapjs-exop-function
  const CTX_SPECIFIC_CLASS = 0b10 << 6;
  const writer = new BerWriter();
  writer.startSequence();
  writer.writeString(dn, CTX_SPECIFIC_CLASS | 0);
  writer.writeString(password, CTX_SPECIFIC_CLASS | 2);
  writer.endSequence();

  await client.exop("1.3.6.1.4.1.4203.1.11.1", writer.buffer);
}

export async function validateUser(username: string, password: string) {
  const client = new Client({
    url: `ldap://${process.env.LDAP_HOST}:3890`
  });

  try {
    const dn = `uid=${username},ou=people,${process.env.LDAP_DC}`;
    await client.bind(dn, password);
    await client.unbind();
    return true;
  } catch (e) {
    return false;
  }
}

export async function checkUserExists(username: string) {
  const users = await getUsers();
  return users.find((u) => u.id.toLowerCase() === username.toLowerCase());
}

export async function getUserInfo(user: User) {
  if (user.username === null) return null;

  const client = await getGraphQLClient();
  const mutation = await client.query({
    query: gql(`
      query GetUser($userId: String!) {
        user(userId: $userId) {
          id
          email
          displayName
          avatar
        }
      }
    `),
    variables: {
      userId: user.username
    }
  });

  const mutationAvatar = mutation.data.user.avatar;

  const avatar = mutationAvatar
    ? `data:image/jpeg;base64,${mutationAvatar}`
    : undefined;

  const dbUser = await prisma.user.findFirst({
    where: {
      username: user.username
    },
    include: {
      discordAuth: true,
      githubAuth: true
    }
  });

  const userInfo: UserInfo = {
    username: mutation.data.user.id,
    displayName: mutation.data.user.displayName,
    email: mutation.data.user.email,
    discordId: dbUser?.discordAuth?.id,
    githubId: dbUser?.githubAuth?.id?.toString(),
    avatar
  };

  return userInfo;
}

export async function updateUser(
  user: User,
  displayName?: string,
  email?: string,
  avatar?: Buffer
) {
  if (user.username === null) return;

  const client = await getGraphQLClient();
  const mutation = await client.mutate({
    mutation: gql(`
      mutation UpdateUser($user: UpdateUserInput!) {
        updateUser(user: $user) {
          ok
        }
      }
    `),
    variables: {
      user: {
        id: user.username,
        displayName,
        email,
        avatar: avatar ? await ensureJpg(avatar) : undefined
      }
    }
  });
}
