import { format } from 'date-fns';
import query from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import { getFullUserIdentities } from './admin.js';
import { getUserById } from './users.js';
import { UserSubscriptionInfo } from '../cron/sendgridSyncFunctions.js';
import { logger } from '../server-utils/logger.js';
import { sendDiscordErrorMessage, sendDiscordMonitoringMessage } from '../cron/notification.js';
import { isEmailable } from '../external-services/emailSender.js';
import { updateEngagmentScoreTable } from './adminEngagement.js';

/**
 * This file contains route handlers for starting the Zoho sync and helper functions for syncing data with Zoho.
 *
 * The zoho sync will be run every time a new user is created, but in order for this to work, we need to get the first access and refresh tokens from Zoho.
 */

/* 
  More info can be found here:
  https://www.zoho.com/crm/developer/docs/api/v3/auth-request.html

  Scopes can be found here:
  https://www.zoho.com/crm/developer/docs/api/v3/scopes.html

  To get a new key, access this site:
  https://api-logger.zoho.com/
*/
type ZohoAccessTokenResponse = {
  access_token: string;
  refresh_token: string;
  scope: string;
  api_domain: string;
  token_type: string;
  expires_in: number;
};

export const getZohoAccessToken = declareHandler({
  func: async (req, res) => {
    const { client_id, client_secret, grant_type, code } = req.query;

    logger.info('Getting access token');
    logger.info('client_id:', client_id);
    logger.info('client_secret:', client_secret);
    logger.info('grant_type:', grant_type);
    logger.info('code:', code);

    let params = new URLSearchParams({
      client_id,
      client_secret,
      grant_type,
      code,
    });

    try {
      const result = await fetch(`https://accounts.zoho.com/oauth/v2/token?${params.toString()}`, {
        method: 'POST',
      });
      const data = await result.json();

      if (data.error) {
        logger.error('Error getting access token:', data.error);
        res.status(500).send({
          message: 'Failed to get access token',
          error: data.error,
        });
        return;
      } else {
        const accessTokenData = data as ZohoAccessTokenResponse;
        // Save the access token to the database
        logger.info('Saving access token to database');
        logger.info(accessTokenData);
        try {
          await query(
            `
            INSERT INTO zoho_tokens (
              access_token,
              refresh_token,
              scope,
              api_domain,
              token_type,
              expires_in,
              client_id,
              client_secret,
              expires_at
            )
            VALUES
              ($1, $2, $3, $4, $5, $6, $7, $8, $9)
            `,
            [
              accessTokenData.access_token,
              accessTokenData.refresh_token,
              accessTokenData.scope,
              accessTokenData.api_domain,
              accessTokenData.token_type,
              accessTokenData.expires_in,
              client_id,
              client_secret,
              new Date(Date.now() + accessTokenData.expires_in * 1000 - 60 * 1000), // 60 seconds before expiration
            ]
          );
        } catch (error) {
          logger.error('Error saving access token to database:', error);
          res.status(500).send({
            message: 'Failed to save access token to database',
            error,
          });
          return;
        }
      }

      res.status(200).send('Access token successfully saved');
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get access token',
        error,
      });
    }
  },
});

export const getLast10ZohoTokens = declareHandler({
  func: async (req, res) => {
    try {
      const result = await query(
        `
        SELECT 
          access_token, 
          refresh_token,
          expires_at > NOW() as is_valid,
          expires_at,
          created_at
        FROM zoho_tokens
        ORDER BY created_at DESC
        LIMIT 10
        `,
        []
      );
      res.status(200).send({
        data: result.rows,
      });
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get last 10 zoho tokens',
        error,
      });
    }
  },
});

export type ZohoToken = {
  access_token: string;
  refresh_token: string;
  client_id: string;
  client_secret: string;
  scope: string;
  api_domain: string;
  token_type: string;
  expires_in: number;
  expires_at: Date;
};

export const getLatestZohoToken = async (): Promise<ZohoToken | null> => {
  const token = await query(
    `
    SELECT 
      access_token, 
      refresh_token,
      client_id,
      client_secret,
      scope,
      api_domain,
      token_type,
      expires_in,
      expires_at
    FROM zoho_tokens
    ORDER BY created_at DESC
    LIMIT 1
    `,
    []
  );
  if (token.rows.length === 0) {
    return null;
  }
  return token.rows[0] as ZohoToken;
};

export type ZohoAccessToken = {
  access_token: string;
};

// Get a valid Zoho token, and refreshes it if it's expired
export const getValidZohoToken = async (): Promise<ZohoAccessToken | null> => {
  const token = await getLatestZohoToken();
  if (token && token.expires_at > new Date()) {
    return {
      access_token: token.access_token,
    };
  }
  // If the token is expired, refresh it
  const refreshResult = await refreshLatestZohoToken();
  if (refreshResult.success) {
    return {
      access_token: refreshResult.token,
    };
  } else {
    return {
      access_token: null,
    };
  }
};

type ZohoRefreshTokenResponse = {
  access_token: string;
  scope: string;
  api_domain: string;
  token_type: string;
  expires_in: number;
};

export const refreshLatestZohoToken = async () => {
  const token = await getLatestZohoToken();
  if (token) {
    const { refresh_token, client_id, client_secret } = token;
    const params = new URLSearchParams({
      client_id,
      client_secret,
      grant_type: 'refresh_token',
      refresh_token,
    });

    logger.info('Refreshing token');
    logger.info('client_id:', client_id);
    logger.info('client_secret:', client_secret);
    logger.info('refresh_token:', refresh_token);

    const result = await fetch(`https://accounts.zoho.com/oauth/v2/token?${params.toString()}`, {
      method: 'POST',
    });
    const data = await result.json();

    if (data.error) {
      logger.error('Error refreshing token:', data.error);
      return {
        success: false,
        error: data.error,
      };
    } else {
      const accessTokenData = data as ZohoRefreshTokenResponse;
      // Save the access token to the database
      logger.info('Saving access token to database');
      logger.info(accessTokenData);
      try {
        await query(
          `
              INSERT INTO zoho_tokens
                (
                access_token,
                refresh_token,
                scope,
                api_domain,
                token_type,
                expires_in,
                client_id,
                client_secret,
                expires_at
                ) 
              VALUES
                ($1, $2, $3, $4, $5, $6, $7, $8, $9)
              `,
          [
            accessTokenData.access_token,
            refresh_token,
            accessTokenData.scope,
            accessTokenData.api_domain,
            accessTokenData.token_type,
            accessTokenData.expires_in,
            client_id,
            client_secret,
            new Date(Date.now() + accessTokenData.expires_in * 1000 - 60 * 1000), // 60 seconds before expiration
          ]
        );
        return {
          success: true,
          token: accessTokenData.access_token,
        };
      } catch (error) {
        logger.error('Error saving access token to database:', error);
        return {
          success: false,
          error,
        };
      }
    }
  } else {
    return {
      success: false,
      error: 'No token found',
    };
  }
};

export const refreshLatestZohoTokenEndpoint = declareHandler({
  func: async (req, res) => {
    const result = await refreshLatestZohoToken();
    if (result.success) {
      res.status(200).send('Token successfully refreshed');
    } else {
      res.status(500).send({
        message: 'Failed to refresh token',
        error: result.error,
      });
    }
  },
});

// Syncs user information to Zoho
const syncUserBasicInfoToZoho = async (userId: number) => {
  logger.info('Syncing user to Zoho:', userId);
  // Get the user

  const userQuery = await query(
    `
    SELECT id AS user_id,
           email,
           display_name,
           browser_language,
           created_at
    FROM users
    WHERE id = $1
    LIMIT 1
  `,
    [userId]
  );

  if (userQuery.rows.length === 0) {
    logger.error('User not found:', userId);
    return {
      success: false,
      error: 'User not found',
    };
  }

  const user = userQuery.rows[0];

  if (!isEmailable(user.email)) {
    logger.info('User is a Musehub user, skipping sync:', userId);
    return {
      success: false,
      error: 'User is a Musehub user',
    };
  }

  const otherUserInfo = await getUserById(userId);

  // Get the latest Zoho token
  const zohoToken = await getValidZohoToken();
  if (!zohoToken) {
    logger.error('No valid Zoho token found');
    return {
      success: false,
      error: 'No valid Zoho token found',
    };
  }
  logger.info('Zoho token found');

  const userName = await getUserNameForZoho(userId, user.username);
  const userData = {
    Email: user.email,
    UserID: `${user.user_id}`,
    Browser_Language: user.browser_language,
    Joined_Date: format(new Date(user.created_at), 'yyyy-MM-dd'),
    Subscription_Plan: otherUserInfo?.plan,
    Subscription_Expires: otherUserInfo?.subscribedUntil
      ? format(new Date(otherUserInfo.subscribedUntil), 'yyyy-MM-dd')
      : null,
  };

  const mergedData = {
    ...userData,
    ...userName,
  };

  // Note on zoho upserting for users:
  // Zoho matches on Email but Last_Name is a mandatory field
  // So we will always need to generate a Last_Name field for each user

  logger.info('Sending data to Zoho:', mergedData);
  const result = await fetch('https://www.zohoapis.com/crm/v3/Contacts/upsert', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Zoho-oauthtoken ${zohoToken.access_token}`,
    },
    body: JSON.stringify({
      data: [mergedData],
    }),
  });

  const data = await result.json();
  logger.info('Zoho response:', data);

  const code = data?.data?.[0]?.code;
  const dataObject = data?.data?.[0];

  if (code === 'SUCCESS') {
    logger.info('User synced to Zoho:', userId);
    return {
      success: true,
    };
  } else {
    return {
      success: false,
      error: dataObject,
    };
  }
};

export const zohoSignupSync = async (userId: number) => {
  const result = await syncUserBasicInfoToZoho(userId);

  const sync_status = result.success ? 'synced' : 'error';
  try {
    await query(
      `
      INSERT INTO zoho_sync_status
        (
        user_id,
        sync_status,
        error_message,
        sync_type
        ) 
      VALUES
        ($1, $2, $3, 'user_signup')
      `,
      [userId, sync_status, result.error]
    );

    await sendDiscordMonitoringMessage(`\`zohoSync.ts\` run.\nZoho sync completed successfully for user ${userId}.`);
  } catch (error) {
    logger.error('Error saving zoho sync log to database:', error);
    await sendDiscordErrorMessage(`\`zohoSync.ts\` error.\nZoho sync failed for user ${userId}.`);
  }
};

const saveZohoUpsertSyncLog = async (
  userIds: number[],
  zohoResponseData?: any,
  sync_type: 'subscription_update' | 'engagement_score' = 'subscription_update'
) => {
  let successCount = 0;
  let errorCount = 0;
  zohoResponseData?.data?.forEach((dataObject) => {
    if (dataObject.code === 'SUCCESS') {
      successCount++;
    } else {
      errorCount++;
    }
  });

  let sync_status = 'synced';
  if (successCount === 0) sync_status = 'error';
  else if (successCount > 0 && errorCount > 0) sync_status = 'partial';

  logger.info('User subscription info synced to Zoho:', successCount, 'successes,', errorCount, 'errors');
  const error = sync_status === 'error' ? zohoResponseData?.data : null;
  try {
    const userIDJson = JSON.stringify(userIds);
    await query(
      `
      INSERT INTO zoho_sync_status
        (
        user_ids,
        sync_status,
        error_message,
        sync_type
        ) 
      VALUES
        ($1, $2, $3, $4)
      `,
      [userIDJson, sync_status, error, sync_type.toString()]
    );
    return sync_status === 'synced';
  } catch (error) {
    logger.error('Error saving zoho sync log to database:', zohoResponseData);
    return false;
  }
};

const ENGAGEMENT_SCORE_THRESHOLD = 1;

export const syncEngagementScoresToZoho = declareHandler({
  func: async (req, res) => {
    const updated = await updateEngagmentScoreTable();

    // Get the latest Zoho token
    const zohoToken = await getValidZohoToken();
    if (!zohoToken) {
      logger.error('No valid Zoho token found');
      return res.status(500).send({ message: 'No valid Zoho token found' });
    }
    logger.info('Zoho token found');

    // Only sync users that are emailable and have a score of more than 1
    const filtered = updated.filter((user) => {
      return user.engagement_score > ENGAGEMENT_SCORE_THRESHOLD && isEmailable(user.email);
    });

    if (filtered.length === 0) {
      logger.info('No users to sync');
      return res.send({ message: 'No users to sync' });
    }

    logger.info('Sending engagement scores to Zoho:', filtered);
    const result = await fetch('https://www.zohoapis.com/crm/v3/Contacts/upsert', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Zoho-oauthtoken ${zohoToken.access_token}`,
      },
      body: JSON.stringify({
        data: filtered.map((user) => {
          return {
            Email: user.email,
            UserID: `${user.user_id}`,
            Engagement_Score: Math.round(user.engagement_score),
            Engagement_Score_Updated: format(new Date(), 'yyyy-MM-dd'),
          };
        }),
      }),
    });

    const responseData = await result.json();
    await saveZohoUpsertSyncLog(
      filtered.map((user) => parseInt(user.user_id)),
      responseData,
      'engagement_score'
    );

    return res.send({ message: 'Engagement scores updated', rows: updated.length });
  },
});

export const syncUserSubscriptionInfoToZoho = async (userSubscriptionInfo: UserSubscriptionInfo[]) => {
  logger.info('Syncing user subscription info to Zoho:', userSubscriptionInfo.length);
  const userIds = userSubscriptionInfo.map((user) => user.user_id);
  // Get the latest Zoho token
  const zohoToken = await getValidZohoToken();
  if (!zohoToken) {
    logger.error('No valid Zoho token found');
    return;
  }
  logger.info('Zoho token found');

  const data = userSubscriptionInfo.map((user) => {
    return {
      Email: user.email,
      UserID: `${user.user_id}`,
      Last_Name: user.display_name,
      Is_Subscribed: user.is_pro,
      Subscription_Plan: user.current_tier,
      Subscription_Started: user.pro_start_date ? format(new Date(user.pro_start_date), 'yyyy-MM-dd') : null,
      Subscription_Expires: user.pro_until_date ? format(new Date(user.pro_until_date), 'yyyy-MM-dd') : null,
      Trial_Expires: user.trial_end_date ? format(new Date(user.trial_end_date), 'yyyy-MM-dd') : null,
      Previous_Subscription_Plan: user.previous_tier,
    };
  });

  // Inefficient but required to get the mandatory Last_Name field
  for (const user of data) {
    const userName = await getUserNameForZoho(parseInt(user.UserID), user.Last_Name);
    user.Last_Name = userName.Last_Name;
  }

  // Note on zoho upserting for users:
  // Zoho matches on Email but Last_Name is a mandatory field
  // So we will always need to generate a Last_Name field for each user

  logger.info('Sending data to Zoho:', data);
  const result = await fetch('https://www.zohoapis.com/crm/v3/Contacts/upsert', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Zoho-oauthtoken ${zohoToken.access_token}`,
    },
    body: JSON.stringify({
      data,
    }),
  });

  const responseData = await result.json();
  return await saveZohoUpsertSyncLog(userIds, responseData);
};

export const syncUserToZohoEndpoint = declareHandler({
  func: async (req, res) => {
    const { userId } = req.params;
    if (!userId) {
      res.status(400).send({
        message: 'User ID is required',
      });
      return;
    }
    const result = await syncUserBasicInfoToZoho(parseInt(userId as string));
    // Log the result to the db

    const sync_status = result.success ? 'synced' : 'error';
    try {
      await query(
        `
        INSERT INTO zoho_sync_status
          (
          user_id,
          sync_status,
          error_message,
          sync_type
          ) 
        VALUES
          ($1, $2, $3, 'manual')
        `,
        [userId, sync_status, result.error]
      );
    } catch (error) {
      logger.error('Error saving zoho sync log to database:', error);
    }
    if (result.success) {
      res.status(200).send({
        message: 'User synced',
      });
    } else {
      res.status(500).send({
        message: 'Failed to sync user',
        error: result.error,
      });
    }
  },
});

export const getRecentUserSyncs = declareHandler({
  func: async (req, res) => {
    const { userId } = req.params;
    if (!userId) {
      res.status(400).send({
        message: 'User ID is required',
      });
      return;
    }
    try {
      const result = await query(
        `
        SELECT 
          id,
          user_id,
          sync_status,
          sync_type,
          created_at
        FROM zoho_sync_status
        WHERE user_id = $1
        ORDER BY created_at DESC
        LIMIT 10
        `,
        [userId]
      );
      res.status(200).send({
        data: result.rows,
      });
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get user syncs',
        error,
      });
    }
  },
});

export const getRecentSyncs = declareHandler({
  func: async (req, res) => {
    try {
      const result = await query(
        `
        SELECT 
          id,
          user_id,
          user_ids,
          sync_status,
          sync_type,
          created_at
        FROM zoho_sync_status
        ORDER BY created_at DESC
        LIMIT 20
        `,
        []
      );
      res.status(200).send({
        data: result.rows,
      });
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get recent syncs',
        error,
      });
    }
  },
});

export const getLatestEngagementScoreSync = declareHandler({
  func: async (req, res) => {
    try {
      const result = await query(
        `
        SELECT 
          created_at
        FROM zoho_sync_status
        WHERE sync_type = 'engagement_score' AND sync_status = 'synced'
        ORDER BY created_at DESC
        LIMIT 1
        `,
        []
      );
      if (result.rows.length === 0) {
        res.status(200).send({
          latest_sync: null,
        });
        return;
      }
      res.status(200).send({
        latest_sync: result.rows[0].created_at,
      });
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get latest engagement score sync',
        error,
      });
    }
  },
});

export const getSyncData = declareHandler({
  func: async (req, res) => {
    const { id } = req.params;
    if (!id) {
      res.status(400).send({
        message: 'Sync ID is required',
      });
      return;
    }
    try {
      const result = await query(
        `
        SELECT 
          id,
          user_id,
          user_ids,
          sync_status,
          error_message,
          created_at
        FROM zoho_sync_status
        WHERE id = $1
        ORDER BY created_at DESC
        LIMIT 1
        `,
        [id]
      );
      res.status(200).send({
        data: result.rows[0],
      });
    } catch (error) {
      res.status(500).send({
        message: 'Failed to get sync data',
        error,
      });
    }
  },
});

export type ZohoUserName = {
  // This is the Zoho expected format
  Last_Name: string;
  First_Name?: string;
  Full_Name?: string;
};

export const getUserNameForZoho = async (userId: number, displayName: string): Promise<ZohoUserName> => {
  const userIdentities = await getFullUserIdentities(userId);

  // Zoho requires a last name field, but the user identities might not have a last name

  // Search for google user identity
  let identity = userIdentities.find((identity) => identity.issuer === 'google');
  // Search for fusionauth user identity
  if (!identity) identity = userIdentities.find((identity) => identity.issuer === 'fusionauth');

  // Search for facebook user identity
  if (!identity) identity = userIdentities.find((identity) => identity.issuer === 'facebook');

  const userName: ZohoUserName = {
    Last_Name: identity?.familyName || identity?.fullName || '<unset>',
  };
  if (identity?.givenName) {
    userName.First_Name = identity.givenName;
  }
  if (identity?.fullName) {
    userName.Full_Name = identity.fullName;
  } else {
    userName.Full_Name = displayName;
  }

  return userName;
};
