/*
 *  This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
 *  License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
 */

// DEVELOPMENT: use scripts/auth/gen-sso.py to generate some test data

import {
  getPassportsCached,
  setPassportsCached,
} from "@cocalc/server/settings/server-settings";
import { to_json } from "@cocalc/util/misc";
import {
  set_account_info_if_different,
  set_account_info_if_not_set,
  set_email_address_verified,
} from "./account-queries";
import {
  CB,
  CreatePassportOpts,
  DeletePassportOpts,
  PassportExistsOpts,
  PostgreSQL,
  UpdateAccountInfoAndPassportOpts,
} from "./types";
import { PassportStrategyDB } from "@cocalc/server/auth/sso/types";
import { isBlockedUnlinkStrategy } from "@cocalc/server/auth/sso/unlink-strategy";

export async function set_passport_settings(
  db: PostgreSQL,
  opts: PassportStrategyDB & { cb?: CB }
): Promise<void> {
  const { strategy, conf, info } = opts;
  let err = null;
  try {
    await db.async_query({
      query: "INSERT INTO passport_settings",
      values: {
        "strategy::TEXT ": strategy,
        "conf    ::JSONB": conf,
        "info    ::JSONB": info,
      },
      conflict: "strategy",
    });
  } catch (err) {
    err = err;
  }
  if (typeof opts.cb === "function") {
    opts.cb(err);
  }
}

export async function get_passport_settings(
  db: PostgreSQL,
  opts: { strategy: string; cb?: CB }
): Promise<any> {
  const { rows } = await db.async_query({
    query: "SELECT conf, info FROM passport_settings",
    where: { "strategy = $::TEXT": opts.strategy },
  });
  if (typeof opts.cb === "function") {
    opts.cb(rows[0]);
  }
  return rows[0];
}

export async function get_all_passport_settings(
  db: PostgreSQL
): Promise<PassportStrategyDB[]> {
  return (
    await db.async_query({
      query: "SELECT strategy, conf, info FROM passport_settings",
    })
  ).rows;
}

export async function get_all_passport_settings_cached(
  db: PostgreSQL
): Promise<PassportStrategyDB[]> {
  const passports = getPassportsCached();
  if (passports != null) {
    return passports;
  }
  const res = await get_all_passport_settings(db);
  setPassportsCached(res);
  return res;
}

// Passports -- accounts linked to Google/Dropbox/Facebook/Github, etc.
// The Schema is slightly redundant, but indexed properly:
//    {passports:['google-id', 'facebook-id'],  passport_profiles:{'google-id':'...', 'facebook-id':'...'}}

export function _passport_key(opts) {
  const { strategy, id } = opts;
  // note: strategy is *our* name of the strategy in the DB, not it's type string!
  if (typeof strategy !== "string") {
    throw new Error("_passport_key: strategy must be defined");
  }
  if (typeof id !== "string") {
    throw new Error("_passport_key: id must be defined");
  }

  return `${strategy}-${id}`;
}

export async function create_passport(
  db: PostgreSQL,
  opts: CreatePassportOpts
): Promise<void> {
  const dbg = db._dbg("create_passport");
  dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });

  try {
    dbg("setting the passport for the account");
    await db.async_query({
      query: "UPDATE accounts",
      jsonb_set: {
        passports: { [_passport_key(opts)]: opts.profile },
      },
      where: {
        "account_id = $::UUID": opts.account_id,
      },
    });

    dbg(
      `setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`
    );
    await set_account_info_if_not_set({
      db: db,
      account_id: opts.account_id,
      email_address: opts.email_address,
      first_name: opts.first_name,
      last_name: opts.last_name,
    });
    // we still record that email address as being verified
    if (opts.email_address != null) {
      await set_email_address_verified({
        db,
        account_id: opts.account_id,
        email_address: opts.email_address,
      });
    }
    opts.cb?.(undefined); // all good
  } catch (err) {
    if (opts.cb != null) {
      opts.cb(err);
    } else {
      throw err;
    }
  }
}

export async function delete_passport(
  db: PostgreSQL,
  opts: DeletePassportOpts
) {
  db._dbg("delete_passport")(to_json({ strategy: opts.strategy, id: opts.id }));

  if (
    await isBlockedUnlinkStrategy({
      strategyName: opts.strategy,
      account_id: opts.account_id,
    })
  ) {
    const err_msg = `You are not allowed to unlink '${opts.strategy}'`;
    if (typeof opts.cb === "function") {
      opts.cb(err_msg);
      return;
    } else {
      throw new Error(err_msg);
    }
  }

  return db._query({
    query: "UPDATE accounts",
    jsonb_set: {
      // delete it
      passports: { [_passport_key(opts)]: null },
    },
    where: {
      "account_id = $::UUID": opts.account_id,
    },
    cb: opts.cb,
  });
}

export async function passport_exists(
  db: PostgreSQL,
  opts: PassportExistsOpts
): Promise<string | undefined> {
  try {
    const result = await db.async_query({
      query: "SELECT account_id FROM accounts",
      where: [
        // this uses the corresponding index to only scan a subset of all accounts!
        "passports IS NOT NULL",
        { "(passports->>$::TEXT) IS NOT NULL": _passport_key(opts) },
      ],
    });
    const account_id = result?.rows[0]?.account_id;
    if (opts.cb != null) {
      opts.cb(null, account_id);
    } else {
      return account_id;
    }
  } catch (err) {
    if (opts.cb != null) {
      opts.cb(err);
    } else {
      throw err;
    }
  }
}

export async function update_account_and_passport(
  db: PostgreSQL,
  opts: UpdateAccountInfoAndPassportOpts
) {
  // we deliberately do not update the email address, because if the SSO
  // strategy sends a different one, this would break the "link".
  // rather, if the email (and hence most likely the email address) changes on the
  // SSO side, this would equal to creating a new account.
  const dbg = db._dbg("update_account_and_passport");
  dbg(
    `updating account info ${to_json({
      first_name: opts.first_name,
      last_name: opts.last_name,
    })}`
  );
  await set_account_info_if_different({
    db: db,
    account_id: opts.account_id,
    first_name: opts.first_name,
    last_name: opts.last_name,
  });
  const key = _passport_key(opts);
  dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);
  await db.async_query({
    query: "UPDATE accounts",
    jsonb_set: {
      passports: { [key]: opts.profile },
    },
    where: {
      "account_id = $::UUID": opts.account_id,
    },
  });
}
