import dotenv from 'dotenv';
import normalizeEmail from 'normalize-email';
import { getUserById, getUserIdByEmail, getUserIdByIssuer } from '../controllers/users.js';
import { restoreSendgridUserIfNeeded, startSendgridOnboarding } from '../cron/sendgridSyncFunctions.js';
import { isEmailable } from '../external-services/emailSender.js';
import { logger } from '../server-utils/logger.js';
import query from '../server-utils/query.js';
import trackServerEvent, { ActivityType } from '../utils/trackServerEvent.js';
import { zohoSignupSync } from './../controllers/zohoSync.js';
import { environmentGreaterOrEqual, environmentPermitsNewUsers } from './environment.js';
import { sendDiscordMonitoringMessage } from '../cron/notification.js';
dotenv.config();

export type NormalisedProfile = {
  id: string;
  email: string;
  displayName: string;
  firstName: string;
  lastName?: string;
};

const normaliseProfile = (issuer, profile): NormalisedProfile => {
  const normalisedProfile: NormalisedProfile = {
    id: profile?.id,
    firstName: '',
    lastName: undefined,
    email: '',
    displayName: '',
  };
  if (profile.email) {
    // FusionAuth
    normalisedProfile.email = profile.email;
  }
  if (!normalisedProfile.email && profile.emails) {
    // Google / Facebook
    normalisedProfile.email = profile.emails[0].value;
  }
  if (issuer.includes('facebook')) {
    normalisedProfile.firstName = profile.displayName;
    normalisedProfile.lastName = undefined;
    normalisedProfile.displayName = profile.displayName;
  } else if (issuer.includes('google')) {
    normalisedProfile.firstName = profile.name.givenName;
    normalisedProfile.lastName = profile.name.familyName;
    normalisedProfile.displayName = profile.displayName;
  } else if (issuer.includes('fusionauth')) {
    normalisedProfile.firstName = profile?.data?.name;
    normalisedProfile.lastName = undefined;
    normalisedProfile.displayName = profile?.data?.name;
  } else if (issuer.includes('musehub')) {
    normalisedProfile.firstName = profile.name;
    normalisedProfile.lastName = undefined;
    normalisedProfile.displayName = profile.name;
  }

  if (!normalisedProfile.displayName && normalisedProfile.email) {
    normalisedProfile.displayName = normalisedProfile.email.split('@')[0];
  }

  return normalisedProfile;
};

// Will log in a user and upsert their user_identities table with the latest profile data.
const performLogin = async (userId, environment, done, issuer, profile, normalisedProfile) => {
  logger.info('Performing login: user ID', userId, 'environment', environment);
  const user = await getUserById(userId);

  if (!user) {
    return done('Error: Could not find user referenced by the matched identity.');
  }

  if (!environmentGreaterOrEqual(user.environment, environment)) {
    return done(null, false, { message: `User is not permitted in ${environment} environment` });
  }

  // Update the user_identities table with the latest profile data, or insert a new row if it doesn't exist.
  // Check if user identity exists
  const userIdentityExists = await query(
    `
      SELECT * FROM user_identities
      WHERE issuer = $1 AND external_id = $2;
    `,
    [issuer, normalisedProfile.id]
  );

  // logger.info('User identity exists', userIdentityExists.rows[0]);
  if (!userIdentityExists.rows.length) {
    logger.info('User identity does not exist');
    // Insert new user identity
    await query(
      `
      INSERT INTO user_identities (user_id, issuer, external_id, profile_data) VALUES ($1, $2, $3, $4);
    `,
      [user.id, issuer, normalisedProfile.id, JSON.stringify(profile)]
    );
  } else {
    logger.info('User identity exists, updating', normalisedProfile);
    // Update existing user identity
    await query(
      `
      UPDATE user_identities SET profile_data = $1 WHERE issuer = $2 AND external_id = $3;
    `,
      [JSON.stringify(profile), issuer, normalisedProfile.id]
    );
  }

  logger.info('done authenticating! (existing user)', user);

  if (isEmailable(normalisedProfile.email)) {
    await restoreSendgridUserIfNeeded(normalisedProfile, userId);
  }

  done(null, user);
};

const createNewUserAndLogin = async (issuer, profile, normalisedProfile, environment, done) => {
  logger.info('Creating new user');
  if (!environmentPermitsNewUsers(environment)) {
    return done(null, false, { message: `${environment} environment does not permit new users` });
  }
  try {
    await query('BEGIN', []);
    // Try to insert user
    logger.info('Inserting user');
    const insertedUser = await query(
      `INSERT INTO users (email, normalized_email, display_name) VALUES ($1, $2, $3) RETURNING id`,
      [normalisedProfile.email, normalizeEmail(normalisedProfile.email), normalisedProfile.displayName]
    );
    const insertedId = insertedUser.rows[0].id;

    logger.info('Inserting user identity');
    await query(
      `
      INSERT INTO user_identities (user_id, issuer, external_id, profile_data) VALUES ($1, $2, $3, $4);
    `,
      [insertedId, issuer, normalisedProfile.id, JSON.stringify(profile)]
    );
    await query('COMMIT', []);

    logger.info('Getting user');
    const user = await getUserById(insertedId);

    if (!user) {
      return done('Error: Could not find user after creating it.');
    }

    if (isEmailable(normalisedProfile.email)) {
      startSendgridOnboarding(normalisedProfile, user.id);
      zohoSignupSync(user.id);
    } else {
      logger.info('Skipping sendgrid onboarding for musehub user');
      sendDiscordMonitoringMessage(
        `New musehub user signed up: ${normalisedProfile.email}. Skipping sendgrid and zoho sync.`
      );
    }

    logger.info('done authenticating! (new user)', user);

    trackServerEvent(user.id, {
      name: ActivityType.Signup,
      properties: {},
    });

    done(null, user);
  } catch (e) {
    await query('ROLLBACK', []);
    done(`Error creating user: ${e.message}`);
    return;
  }
};

// This will match a user based on their issuer + external_id,
// if no match is found, it will check by email.
export const runLoginAuthProvider = async (issuer, profile, done) => {
  const environment = process.env.ENVIRONMENT || 'development';

  let normalisedProfile = normaliseProfile(issuer, profile);

  const existingUserId = await getUserIdByIssuer(issuer, profile.id);
  if (existingUserId) {
    logger.info('Existing user found by issuer', issuer, profile.id);
    logger.info('done authenticating! (existing user)', existingUserId);
    trackServerEvent(existingUserId, {
      name: ActivityType.Login,
      properties: {},
    });
    return await performLogin(existingUserId, environment, done, issuer, profile, normalisedProfile);
  }

  logger.info('No user found by issuer, checking by email');
  if (!normalisedProfile.email) {
    logger.info('profile is missing email address:', profile);
    return done('Error: No email address found in profile.');
  }

  const existingUserIdByEmail = await getUserIdByEmail(normalisedProfile.email);
  if (existingUserIdByEmail) {
    logger.info('Existing user found by email', normalisedProfile.email);
    logger.info('done authenticating! (existing user)', existingUserId);
    trackServerEvent(existingUserIdByEmail, {
      name: ActivityType.AlternateProviderLogin,
      properties: {},
    });
    return await performLogin(existingUserIdByEmail, environment, done, issuer, profile, normalisedProfile);
  }

  logger.info('No user found by email, completing with error.');
  return done(null);
};

// This will match a user based on their email, or issuer + external_id,
// and create a new user if no match is found.
const runRegistrationAuthProvider = async (issuer, profile, done) => {
  const environment = process.env.ENVIRONMENT || 'development';

  let normalisedProfile = normaliseProfile(issuer, profile);
  if (!normalisedProfile.email) {
    logger.info('profile is missing email address:', profile);
    return done('Error: No email address found in profile.');
  }

  let existingUserId = await getUserIdByIssuer(issuer, normalisedProfile.id);
  if (existingUserId) {
    trackServerEvent(existingUserId, {
      name: ActivityType.Login,
      properties: {},
    });
    await performLogin(existingUserId, environment, done, issuer, profile, normalisedProfile);
  } else {
    existingUserId = await getUserIdByEmail(normalisedProfile.email);
    if (existingUserId) {
      // User exists, but has not logged in from this provider before
      trackServerEvent(existingUserId, {
        name: ActivityType.AlternateProviderSignup,
        properties: {},
      });
      await performLogin(existingUserId, environment, done, issuer, profile, normalisedProfile);
    } else {
      // Completely new user
      await createNewUserAndLogin(issuer, profile, normalisedProfile, environment, done);
    }
  }
};

export default runRegistrationAuthProvider;
