
import {
  Attestation,
  Email,
  EmailParam,
  OauthProviderParams,
  OidcTokenParam,
  PublicKeyParam,
  UserNameParam,
  Wallet,
} from '../types/turnkey.js';
import {
  ApiKeyStamper,
  DEFAULT_ETHEREUM_ACCOUNTS,
  DEFAULT_SOLANA_ACCOUNTS,
  TurnkeyServerClient,
} from '@turnkey/sdk-server';
import { decode, JwtPayload } from 'jsonwebtoken';
import { TURNKEY_APP_CONFIG } from '../config/index.js';

export const TURNKEY_SESSION_EXPIRE = '7200'; // 2 hours in seconds

export interface ITurnkeyConfig {
  apiBaseUrl: string;
  defaultOrganizationId: string;
  rpId: string;
  iframeUrl: string;
}

export const getTurnkeyConfig = (baseUrl: string, organizationId: string, rpId: string): ITurnkeyConfig => ({
  apiBaseUrl: baseUrl,
  defaultOrganizationId: organizationId,
  rpId,
  iframeUrl: 'https://auth.turnkey.com',
});

export const TURNKEY_EMBEDDED_KEY = "@turnkey/embedded_key";
export const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 24 * 365 * 10; // 10 years in milliseconds

export const TURNKEY_CREDENTIAL_BUNDLE = "@turnkey/credential_bundle";
export const TURNKEY_CREDENTIAL_BUNDLE_TTL_IN_MILLIS =  1000 * 60 * 60 * 24 * 365 * 10; // 48 hours in milliseconds;

export const getApiKeyStamper = (apiPublicKey: string, apiPrivateKey: string) => new ApiKeyStamper({
  apiPublicKey,
  apiPrivateKey,
});

const getTurnkeyClient = (baseUrl: string, organizationId: string, stamper: ApiKeyStamper) =>
  new TurnkeyServerClient({
    apiBaseUrl: baseUrl,
    organizationId,
    stamper,
  });

function decodeJwt(credential: string): JwtPayload | null {
  const decoded = decode(credential);

  if (decoded && typeof decoded === 'object' && 'email' in decoded) {
    return decoded as JwtPayload;
  }

  return null;
}

export const oauth = async ({
  credential,
  targetPublicKey,
  targetSubOrgId,
  config,
  stamper,
}: {
  credential: string;
  targetPublicKey: string;
  targetSubOrgId: string;
  config: ITurnkeyConfig;
  stamper: ApiKeyStamper;
}) => {
  const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
  const oauthResponse = await client.oauth({
    oidcToken: credential,
    targetPublicKey,
    organizationId: targetSubOrgId,
    expirationSeconds: TURNKEY_SESSION_EXPIRE,
  });

  return oauthResponse;
};

export async function getSubOrgId(
  config: ITurnkeyConfig,
  param: OidcTokenParam | PublicKeyParam | EmailParam | UserNameParam,
  stamper: ApiKeyStamper,
) {
  let filterType: string;
  let filterValue: string;
  if ('userName' in param) {
    filterType = 'USERNAME';
    filterValue = param.userName;
  } else if ('email' in param) {
    filterType = 'EMAIL';
    filterValue = param.email;
  } else if ('publicKey' in param) {
    filterType = 'PUBLIC_KEY';
    filterValue = param.publicKey;
  } else if ('oidcToken' in param) {
    filterType = 'OIDC_TOKEN';
    filterValue = param.oidcToken;
  } else {
    throw new Error('Invalid parameter');
  }

  const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
  const { organizationIds } = await client.getSubOrgIds({
    organizationId: config.defaultOrganizationId,
    filterType,
    filterValue,
  });
  return organizationIds[0];
}

export const getSubOrgIdByEmail = async (config: ITurnkeyConfig, email: Email, stamper: ApiKeyStamper) => {
  return getSubOrgId(config, { email }, stamper);
};

export const createUserSubOrg = async ({
  config,
  username,
  email,
  passkey,
  oauth,
  delegationApiKey,
  onSuccess,
  stamper,
}: {
  config: ITurnkeyConfig;
  username?: string;
  email?: Email;
  passkey?: {
    challenge: string;
    attestation: Attestation;
  };
  oauth?: OauthProviderParams;
  delegationApiKey?: string;
  onSuccess?: (subOrg: {
    subOrganizationId: string;
  }, delegationApiKey: string) => void;
  stamper: ApiKeyStamper;
}) => {
  const authenticators = passkey
    ? [
        {
          authenticatorName: 'Passkey',
          challenge: passkey.challenge,
          attestation: passkey.attestation,
        },
      ]
    : [];

  const oauthProviders = oauth
    ? [
        {
          providerName: oauth.providerName,
          oidcToken: oauth.oidcToken,
        },
      ]
    : [];


  let userEmail = email;
  // If the user is logging in with a Google Auth credential, use the email from the decoded OIDC token (credential
  // Otherwise, use the email from the email parameter
  if (oauth) {
    const decoded = decodeJwt(oauth.oidcToken);
    if (decoded?.email) {
      userEmail = decoded.email;
    }
  }
  const subOrganizationName = `Sub Org - ${email ?? ''}`;
  const userName = username ? username : email ? email.split('@')?.[0] || email : '';

  const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
  const subOrg = await client.createSubOrganization({
    organizationId: config.defaultOrganizationId,
    subOrganizationName,
    rootUsers: [
      {
        userName,
        userEmail,
        oauthProviders,
        authenticators,
        apiKeys: delegationApiKey ? [{
          apiKeyName: TURNKEY_APP_CONFIG.BOT_DELEGATION_NAME,
          publicKey: delegationApiKey,
          curveType: 'API_KEY_CURVE_P256',
        }] : [],
      },
    ],
    rootQuorumThreshold: 1,
    wallet: {
      walletName: 'Default Wallet',
      accounts: [...DEFAULT_ETHEREUM_ACCOUNTS, ...DEFAULT_SOLANA_ACCOUNTS],
    },
  });
  const userId = subOrg.rootUserIds?.[0];
  if (!userId) {
    throw new Error('No root user ID found');
  }
  const { user } = await client.getUser({
    organizationId: subOrg.subOrganizationId,
    userId,
  });

  if(delegationApiKey && onSuccess) {
    await onSuccess?.(subOrg, delegationApiKey);
  }

  return { subOrg, user };
};

export const getSubOrgIdByPublicKey = async (config: ITurnkeyConfig, publicKey: string, stamper: ApiKeyStamper) => {
  return getSubOrgId(config, { publicKey }, stamper);
};

export const getSubOrgIdByUserName = async (config: ITurnkeyConfig, userName: string, stamper: ApiKeyStamper) => {
  return getSubOrgId(config, { userName: userName.toLowerCase() }, stamper);
};

export async function getWalletsWithAccounts(config: ITurnkeyConfig, organizationId: string, stamper: ApiKeyStamper): Promise<Wallet[]> {
  try {
    const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
    const { wallets } = await client.getWallets({
      organizationId,
    });

    const walletsWithAccounts = await Promise.all(
      wallets.map(async (wallet) => {
        try {
          const { accounts } = await client.getWalletAccounts({
            organizationId,
            walletId: wallet.walletId,
          });
          return { ...wallet, accounts };
        } catch (error) {
          console.error(`Failed to fetch accounts for wallet ${wallet.walletId}:`, error);
          // Return wallet with empty accounts array if account fetch fails
          return { ...wallet, accounts: [] };
        }
      }),
    );

    return walletsWithAccounts;
  } catch (error) {
    console.error('Failed to fetch wallets:', error);
    throw new Error('Failed to fetch wallets and accounts. Please try again later.');
  }
}

export async function getUserOrganization(config: ITurnkeyConfig, organizationId: string, stamper: ApiKeyStamper) {
  const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
  const data = await client.getOrganization({
    organizationId,
  });

  return data;
}

export async function initEmailRecovery(
  config: ITurnkeyConfig,
  organizationId: string,
  email: string,
  targetPublicKey: string,
  stamper: ApiKeyStamper,
) {
  const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper);
  const data = await client.initUserEmailRecovery({
    organizationId,
    email: email,
    targetPublicKey: targetPublicKey,
    emailCustomization: {
      appName: 'Gun.fun',
      logoUrl: 'https://www.gun.fun/images/logo.svg',
    },
  });

  return data;
}
