/* eslint-disable unicorn/no-null,max-params */
import { createHash, randomBytes } from "node:crypto";
import omit from "lodash/omit.js";
import { IMetamigoRepositories, idKeysOf } from "../records/index.js";
import type { UnsavedAccount } from "../records/account.js";
import type { UserId, UnsavedUser, SavedUser } from "../records/user.js";
import type { UnsavedSession, SavedSession } from "../records/session.js";
import {
  AdapterAccount,
  AdapterSession,
  AdapterUser,
} from "next-auth/adapters.js";
import { ReadableStreamDefaultController } from "stream/web";

// Sessions expire after 30 days of being idle
export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000;
// Sessions updated only if session is greater than this value (0 = always)
export const defaulteSessionUpdateAge = 24 * 60 * 60 * 1000;

const getCompoundId = (providerId: any, providerAccountId: any) =>
  createHash("sha256")
    .update(`${providerId}:${providerAccountId}`)
    .digest("hex");

const randomToken = () => randomBytes(32).toString("hex");

export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
  constructor(
    private repos: TRepositories,
    private readonly sessionMaxAge = defaultSessionMaxAge,
    private readonly sessionUpdateAge = defaulteSessionUpdateAge
  ) { }

  async createUser(profile: UnsavedUser): Promise<SavedUser> {
    // @ts-ignore Typescript doesn't like lodash's omit()
    return this.repos.users.upsert(omit(profile, ["isActive", "id"]));
  }

  async getUser(id: UserId): Promise<SavedUser | null> {
    const user = await this.repos.users.findById({ id });
    if (!user) return null;
    // if a user has no linked accounts, then we do not return it
    // see: https://github.com/nextauthjs/next-auth/issues/876
    const accounts = await this.repos.accounts.findAllBy({
      userId: user.id,
    });

    if (!accounts || accounts.length === 0) return null;
    return user;
  }

  async getUserByEmail(email: string): Promise<SavedUser | null> {
    const user = await this.repos.users.findBy({ email });
    if (!user) return null;
    // if a user has no linked accounts, then we do not return it
    // see: https://github.com/nextauthjs/next-auth/issues/876
    const accounts = await this.repos.accounts.findAllBy({
      userId: user.id,
    });

    if (!accounts || accounts.length === 0) return null;
    return user;
  }

  async getUserByAccount(
    provider: string,
    providerAccountId: string
  ): Promise<SavedUser | null> {
    const account = await this.repos.accounts.findBy({
      compoundId: getCompoundId(provider, providerAccountId),
    });
    if (!account) return null;

    return this.repos.users.findById({ id: account.userId });
  }

  async updateUser(user: SavedUser): Promise<SavedUser> {
    return this.repos.users.update(user);
  }

  async linkAccount(adapterAccount: AdapterAccount): Promise<void> {
    const {
      userId,
      access_token: accessToken,
      refresh_token: refreshToken,
      provider: providerId,
      providerAccountId,
      expires_at: accessTokenExpires,
      type: providerType,
    } = adapterAccount;
    const exists = await this.repos.users.existsById({ id: userId });
    if (!exists) return;
    const account: UnsavedAccount = {
      accessToken,
      refreshToken,
      compoundId: getCompoundId(providerId, providerAccountId),
      providerAccountId,
      providerId,
      providerType,
      accessTokenExpires: accessTokenExpires
        ? new Date(accessTokenExpires)
        : new Date(),
      userId,
    };
    await this.repos.accounts.insert(account);
  }

  async unlinkAccount(
    userId: string,
    providerId: string,
    providerAccountId: string
  ): Promise<void> {
    await this.repos.accounts.removeBy({
      userId,
      compoundId: getCompoundId(providerId, providerAccountId),
    });
  }

  createSession({
    sessionToken,
    userId,
  }: {
    sessionToken: string;
    userId: string;
  }): Promise<SavedSession> {
    let expires;
    if (this.sessionMaxAge) {
      const dateExpires = new Date(Date.now() + this.sessionMaxAge);
      expires = dateExpires.toISOString();
    }

    const session: UnsavedSession = {
      expires,
      userId,
      sessionToken,
      //sessionToken: randomToken(),
      accessToken: randomToken(),
    };

    return this.repos.sessions.insert(session);
  }

  async getSessionAndUser(
    sessionToken: string
  ): Promise<{ session: AdapterSession; user: any; } | null> {
    const session = await this.repos.sessions.findBy({ sessionToken });
    if (!session) return null;
    if (session && session.expires && new Date() > session.expires) {
      this.repos.sessions.remove(session);
      return null;
    }

    const user = await this.repos.users.findById({ id: session.userId });
    if (!user) return null;

    const adapterSession: AdapterSession = {
      userId: session.userId,
      expires: session.expires,
      sessionToken: sessionToken,
    };

    const adapterUser: any = {
      id: user.id,
      email: user.email,
      emailVerified: user.emailVerified,
      userRole: user.userRole
    };

    return { session: adapterSession, user: adapterUser };
  }

  async updateSession(
    session: SavedSession,
    force?: boolean
  ): Promise<SavedSession | null> {
    if (
      this.sessionMaxAge &&
      (this.sessionUpdateAge || this.sessionUpdateAge === 0) &&
      session.expires
    ) {
      // Calculate last updated date, to throttle write updates to database
      // Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
      //     e.g. ({expiry date} - 30 days) + 1 hour
      //
      // Default for sessionMaxAge is 30 days.
      // Default for sessionUpdateAge is 1 hour.
      const dateSessionIsDueToBeUpdated = new Date(
        session.expires.getTime() - this.sessionMaxAge + this.sessionUpdateAge
      );

      // Trigger update of session expiry date and write to database, only
      // if the session was last updated more than {sessionUpdateAge} ago
      if (new Date() > dateSessionIsDueToBeUpdated) {
        const newExpiryDate = new Date();
        newExpiryDate.setTime(newExpiryDate.getTime() + this.sessionMaxAge);
        session.expires = newExpiryDate;
      } else if (!force) {
        return null;
      }
    } else if (!force) {
      // If session MaxAge, session UpdateAge or session.expires are
      // missing then don't even try to save changes, unless force is set.
      return null;
    }

    const { expires } = session;
    return this.repos.sessions.update({ ...session, expires });
  }

  async deleteSession(sessionToken: string): Promise<void> {
    await this.repos.sessions.removeBy({ sessionToken });
  }
}
