/**
 * Social Auth Service
 *
 * Handles OAuth flows for social media platform integrations.
 * Phase 1: Meta (Facebook/Instagram) OAuth implementation
 *
 * Features:
 * - OAuth authorization URL generation with CSRF protection
 * - Callback handling with token exchange
 * - Token refresh for expired access tokens
 * - Token encryption using AES-256-GCM
 * - Account management (list, revoke)
 *
 * @module lib/services/social-auth.service
 */

import crypto from 'crypto';
import { prisma } from '@/lib/db';
import { logger } from '@/lib/logger';
import { encrypt, decrypt } from '@/lib/encryption';
import type {
  SocialPlatform,
  SocialIntegrationStatus,
  SocialAccountOutput,
  TokenData,
  OAuthState,
} from '@/lib/types/social-media.types';

// Meta Graph API version
const META_API_VERSION = 'v19.0';
const META_GRAPH_URL = `https://graph.facebook.com/${META_API_VERSION}`;

// OAuth state expiry (15 minutes)
const STATE_EXPIRY_MS = 15 * 60 * 1000;

// Token refresh buffer (5 minutes before expiry)
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;

/**
 * Get Meta App credentials from environment
 */
function getMetaCredentials(): { appId: string; appSecret: string } {
  const appId = process.env.META_APP_ID;
  const appSecret = process.env.META_APP_SECRET;

  if (!appId || !appSecret) {
    throw new Error('META_APP_ID and META_APP_SECRET must be configured');
  }

  return { appId, appSecret };
}

/**
 * Generate a secure CSRF token
 */
function generateCsrfToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

/**
 * Encode OAuth state with encryption
 */
function encodeOAuthState(state: OAuthState): string {
  const stateJson = JSON.stringify(state);
  return Buffer.from(stateJson).toString('base64url');
}

/**
 * Decode and validate OAuth state
 */
function decodeOAuthState(encodedState: string): OAuthState | null {
  try {
    const stateJson = Buffer.from(encodedState, 'base64url').toString('utf-8');
    const state = JSON.parse(stateJson) as OAuthState;

    // Validate state has required fields
    if (!state.userId || !state.platform || !state.csrfToken || !state.timestamp) {
      logger.warn('Invalid OAuth state: missing required fields');
      return null;
    }

    // Check if state has expired
    if (Date.now() - state.timestamp > STATE_EXPIRY_MS) {
      logger.warn('OAuth state expired', {
        userId: state.userId,
        platform: state.platform,
        age: Date.now() - state.timestamp,
      });
      return null;
    }

    return state;
  } catch (error) {
    logger.error('Failed to decode OAuth state', { error });
    return null;
  }
}

/**
 * Get authorization URL for a social platform
 *
 * Generates OAuth authorization URL with proper scopes and CSRF protection.
 * Currently supports Meta (Facebook/Instagram) platforms.
 *
 * @param platform - Target social platform
 * @param userId - User ID initiating the connection
 * @param redirectUrl - OAuth callback URL
 * @returns Authorization URL and CSRF token
 */
export async function getAuthorizationUrl(
  platform: SocialPlatform,
  userId: string,
  redirectUrl: string
): Promise<{ url: string; csrfToken: string }> {
  const csrfToken = generateCsrfToken();

  const state: OAuthState = {
    userId,
    platform,
    csrfToken,
    redirectUrl,
    timestamp: Date.now(),
  };

  const encodedState = encodeOAuthState(state);

  if (platform === 'FACEBOOK' || platform === 'INSTAGRAM') {
    const { appId } = getMetaCredentials();

    // Combined scopes for both Facebook Pages and Instagram Business
    const scopes = [
      'pages_manage_posts',
      'pages_read_engagement',
      'pages_show_list',
      'pages_messaging',
      'instagram_basic',
      'instagram_content_publish',
      'instagram_manage_insights',
      'business_management',
    ].join(',');

    const params = new URLSearchParams({
      client_id: appId,
      redirect_uri: redirectUrl,
      state: encodedState,
      scope: scopes,
      response_type: 'code',
    });

    const url = `https://www.facebook.com/${META_API_VERSION}/dialog/oauth?${params}`;

    logger.info('Generated Meta OAuth URL', {
      userId,
      platform,
      redirectUrl,
    });

    return { url, csrfToken };
  }

  // Twitter and TikTok support planned for Phase 2
  throw new Error(`Platform ${platform} OAuth not yet implemented`);
}

/**
 * Exchange authorization code for access token (Meta)
 */
async function exchangeMetaCode(
  code: string,
  redirectUri: string
): Promise<{
  accessToken: string;
  expiresIn: number;
}> {
  const { appId, appSecret } = getMetaCredentials();

  const params = new URLSearchParams({
    client_id: appId,
    client_secret: appSecret,
    redirect_uri: redirectUri,
    code,
  });

  const response = await fetch(
    `${META_GRAPH_URL}/oauth/access_token?${params}`
  );

  if (!response.ok) {
    const error = await response.json();
    logger.error('Meta token exchange failed', { error });
    throw new Error(error.error?.message || 'Failed to exchange authorization code');
  }

  const data = await response.json();

  return {
    accessToken: data.access_token,
    expiresIn: data.expires_in || 3600,
  };
}

/**
 * Exchange short-lived token for long-lived token (Meta)
 */
async function getLongLivedToken(shortLivedToken: string): Promise<{
  accessToken: string;
  expiresIn: number;
}> {
  const { appId, appSecret } = getMetaCredentials();

  const params = new URLSearchParams({
    grant_type: 'fb_exchange_token',
    client_id: appId,
    client_secret: appSecret,
    fb_exchange_token: shortLivedToken,
  });

  const response = await fetch(
    `${META_GRAPH_URL}/oauth/access_token?${params}`
  );

  if (!response.ok) {
    const error = await response.json();
    logger.error('Failed to get long-lived token', { error });
    throw new Error(error.error?.message || 'Failed to get long-lived token');
  }

  const data = await response.json();

  return {
    accessToken: data.access_token,
    expiresIn: data.expires_in || 5184000, // ~60 days default
  };
}

/**
 * Get user's Facebook pages with their Instagram business accounts
 */
async function getMetaAccounts(accessToken: string): Promise<Array<{
  pageId: string;
  pageName: string;
  pageAccessToken: string;
  instagramAccountId?: string;
  instagramUsername?: string;
}>> {
  // Get user's pages
  const pagesResponse = await fetch(
    `${META_GRAPH_URL}/me/accounts?fields=id,name,access_token,instagram_business_account&access_token=${accessToken}`
  );

  if (!pagesResponse.ok) {
    const error = await pagesResponse.json();
    logger.error('Failed to fetch Meta pages', { error });
    throw new Error(error.error?.message || 'Failed to fetch pages');
  }

  const pagesData = await pagesResponse.json();
  const accounts: Array<{
    pageId: string;
    pageName: string;
    pageAccessToken: string;
    instagramAccountId?: string;
    instagramUsername?: string;
  }> = [];

  for (const page of pagesData.data || []) {
    const account: {
      pageId: string;
      pageName: string;
      pageAccessToken: string;
      instagramAccountId?: string;
      instagramUsername?: string;
    } = {
      pageId: page.id,
      pageName: page.name,
      pageAccessToken: page.access_token,
    };

    // If page has Instagram business account, get its details
    if (page.instagram_business_account?.id) {
      const igResponse = await fetch(
        `${META_GRAPH_URL}/${page.instagram_business_account.id}?fields=id,username&access_token=${page.access_token}`
      );

      if (igResponse.ok) {
        const igData = await igResponse.json();
        account.instagramAccountId = igData.id;
        account.instagramUsername = igData.username;
      }
    }

    accounts.push(account);
  }

  return accounts;
}

/**
 * Handle OAuth callback and create social accounts
 *
 * Exchanges the authorization code for tokens, fetches available accounts,
 * and stores them with encrypted tokens.
 *
 * @param platform - The social platform
 * @param code - Authorization code from OAuth redirect
 * @param state - Encoded OAuth state
 * @returns Array of created social accounts
 */
export async function handleCallback(
  platform: SocialPlatform,
  code: string,
  state: string
): Promise<SocialAccountOutput[]> {
  // Decode and validate state
  const oauthState = decodeOAuthState(state);
  if (!oauthState) {
    throw new Error('Invalid or expired OAuth state');
  }

  if (oauthState.platform !== platform) {
    throw new Error('Platform mismatch in OAuth state');
  }

  const { userId, redirectUrl } = oauthState;

  logger.info('Processing OAuth callback', { userId, platform });

  if (platform === 'FACEBOOK' || platform === 'INSTAGRAM') {
    // Exchange code for short-lived token
    const { accessToken: shortToken } = await exchangeMetaCode(code, redirectUrl);

    // Exchange for long-lived token
    const { accessToken, expiresIn } = await getLongLivedToken(shortToken);

    // Get available pages and Instagram accounts
    const metaAccounts = await getMetaAccounts(accessToken);

    if (metaAccounts.length === 0) {
      throw new Error('No Facebook Pages found. Please ensure you have admin access to at least one Page.');
    }

    const createdAccounts: SocialAccountOutput[] = [];

    for (const account of metaAccounts) {
      // Determine expiry date
      const tokenExpiry = new Date(Date.now() + expiresIn * 1000);

      // Create Facebook Page account
      const fbAccount = await prisma.social_accounts.upsert({
        where: {
          userId_platform_platformAccountId: {
            userId,
            platform: 'FACEBOOK',
            platformAccountId: account.pageId,
          },
        },
        update: {
          accountName: account.pageName,
          accessToken: encrypt(account.pageAccessToken),
          tokenExpiry,
          status: 'CONNECTED',
          metadata: { userAccessToken: encrypt(accessToken) },
          updatedAt: new Date(),
        },
        create: {
          userId,
          platform: 'FACEBOOK',
          platformAccountId: account.pageId,
          accountName: account.pageName,
          accessToken: encrypt(account.pageAccessToken),
          tokenExpiry,
          status: 'CONNECTED',
          metadata: { userAccessToken: encrypt(accessToken) },
        },
      });

      createdAccounts.push(transformAccount(fbAccount));

      // Create Instagram account if linked
      if (account.instagramAccountId && account.instagramUsername) {
        const igAccount = await prisma.social_accounts.upsert({
          where: {
            userId_platform_platformAccountId: {
              userId,
              platform: 'INSTAGRAM',
              platformAccountId: account.instagramAccountId,
            },
          },
          update: {
            accountName: `@${account.instagramUsername}`,
            accessToken: encrypt(account.pageAccessToken), // Use page token for IG API
            tokenExpiry,
            status: 'CONNECTED',
            metadata: {
              linkedPageId: account.pageId,
              username: account.instagramUsername,
            },
            updatedAt: new Date(),
          },
          create: {
            userId,
            platform: 'INSTAGRAM',
            platformAccountId: account.instagramAccountId,
            accountName: `@${account.instagramUsername}`,
            accessToken: encrypt(account.pageAccessToken),
            tokenExpiry,
            status: 'CONNECTED',
            metadata: {
              linkedPageId: account.pageId,
              username: account.instagramUsername,
            },
          },
        });

        createdAccounts.push(transformAccount(igAccount));
      }
    }

    logger.info('OAuth callback processed successfully', {
      userId,
      platform,
      accountsCreated: createdAccounts.length,
    });

    return createdAccounts;
  }

  throw new Error(`Platform ${platform} OAuth not yet implemented`);
}

/**
 * Refresh access token for a social account
 *
 * For Meta platforms, refreshes the long-lived token before it expires.
 *
 * @param accountId - Social account ID
 * @returns Updated token data
 */
export async function refreshToken(accountId: string): Promise<TokenData> {
  const account = await prisma.social_accounts.findUnique({
    where: { id: accountId },
  });

  if (!account) {
    throw new Error('Social account not found');
  }

  // Check if token refresh is needed
  if (account.tokenExpiry && account.tokenExpiry.getTime() > Date.now() + TOKEN_REFRESH_BUFFER_MS) {
    // Token still valid, return current data
    return {
      accessToken: decrypt(account.accessToken),
      expiresAt: account.tokenExpiry,
    };
  }

  logger.info('Refreshing social account token', {
    accountId,
    platform: account.platform,
  });

  if (account.platform === 'FACEBOOK' || account.platform === 'INSTAGRAM') {
    const currentToken = decrypt(account.accessToken);

    // For Meta, refresh by exchanging current long-lived token
    const { accessToken, expiresIn } = await getLongLivedToken(currentToken);
    const tokenExpiry = new Date(Date.now() + expiresIn * 1000);

    // Update stored token
    await prisma.social_accounts.update({
      where: { id: accountId },
      data: {
        accessToken: encrypt(accessToken),
        tokenExpiry,
        status: 'CONNECTED',
        updatedAt: new Date(),
      },
    });

    logger.info('Token refreshed successfully', { accountId });

    return {
      accessToken,
      expiresAt: tokenExpiry,
    };
  }

  throw new Error(`Token refresh not implemented for platform ${account.platform}`);
}

/**
 * Get decrypted access token for a social account
 *
 * Automatically refreshes if token is expired or about to expire.
 *
 * @param accountId - Social account ID
 * @returns Valid access token
 */
export async function getAccessToken(accountId: string): Promise<string> {
  const tokenData = await refreshToken(accountId);
  return tokenData.accessToken;
}

/**
 * Revoke access and disconnect a social account
 *
 * @param accountId - Social account ID
 */
export async function revokeAccess(accountId: string): Promise<void> {
  const account = await prisma.social_accounts.findUnique({
    where: { id: accountId },
  });

  if (!account) {
    throw new Error('Social account not found');
  }

  logger.info('Revoking social account access', {
    accountId,
    platform: account.platform,
  });

  // For Meta platforms, revoke the token via API
  if (account.platform === 'FACEBOOK' || account.platform === 'INSTAGRAM') {
    try {
      const accessToken = decrypt(account.accessToken);

      // Revoke permissions
      await fetch(
        `${META_GRAPH_URL}/me/permissions?access_token=${accessToken}`,
        { method: 'DELETE' }
      );
    } catch (error) {
      // Log but don't fail - token might already be invalid
      logger.warn('Failed to revoke Meta permissions', { accountId, error });
    }
  }

  // Delete the account from database
  await prisma.social_accounts.delete({
    where: { id: accountId },
  });

  logger.info('Social account disconnected', { accountId });
}

/**
 * Get all connected social accounts for a user
 *
 * @param userId - User ID
 * @returns Array of connected accounts (without sensitive token data)
 */
export async function getConnectedAccounts(userId: string): Promise<SocialAccountOutput[]> {
  const accounts = await prisma.social_accounts.findMany({
    where: { userId },
    orderBy: { createdAt: 'desc' },
  });

  return accounts.map(transformAccount);
}

/**
 * Get a single social account by ID
 *
 * @param accountId - Social account ID
 * @param userId - User ID (for authorization)
 * @returns Social account or null
 */
export async function getAccountById(
  accountId: string,
  userId: string
): Promise<SocialAccountOutput | null> {
  const account = await prisma.social_accounts.findFirst({
    where: {
      id: accountId,
      userId,
    },
  });

  return account ? transformAccount(account) : null;
}

/**
 * Update account status (used by token refresh jobs)
 *
 * @param accountId - Social account ID
 * @param status - New status
 * @param errorMessage - Optional error message
 */
export async function updateAccountStatus(
  accountId: string,
  status: SocialIntegrationStatus,
  errorMessage?: string
): Promise<void> {
  await prisma.social_accounts.update({
    where: { id: accountId },
    data: {
      status,
      metadata: errorMessage
        ? { ...(await getAccountMetadata(accountId)), lastError: errorMessage }
        : undefined,
      updatedAt: new Date(),
    },
  });

  logger.info('Updated social account status', { accountId, status });
}

/**
 * Get account metadata
 */
async function getAccountMetadata(accountId: string): Promise<Record<string, unknown>> {
  const account = await prisma.social_accounts.findUnique({
    where: { id: accountId },
    select: { metadata: true },
  });

  return (account?.metadata as Record<string, unknown>) || {};
}

/**
 * Transform database account to output type (strip sensitive data)
 */
function transformAccount(account: {
  id: string;
  userId: string;
  platform: string;
  platformAccountId: string;
  accountName: string;
  status: string;
  tokenExpiry: Date | null;
  metadata: unknown;
  createdAt: Date;
  updatedAt: Date;
}): SocialAccountOutput {
  return {
    id: account.id,
    userId: account.userId,
    platform: account.platform as SocialPlatform,
    platformAccountId: account.platformAccountId,
    accountName: account.accountName,
    status: account.status as SocialIntegrationStatus,
    tokenExpiry: account.tokenExpiry,
    metadata: (account.metadata as Record<string, unknown>) || null,
    createdAt: account.createdAt,
    updatedAt: account.updatedAt,
  };
}

/**
 * Validate CSRF token from stored state
 *
 * @param state - Encoded OAuth state
 * @param csrfToken - CSRF token to validate
 * @returns True if valid
 */
export function validateCsrfToken(state: string, csrfToken: string): boolean {
  const oauthState = decodeOAuthState(state);
  if (!oauthState) {
    return false;
  }

  // Use timing-safe comparison to prevent timing attacks
  const tokenBuffer = Buffer.from(csrfToken);
  const stateBuffer = Buffer.from(oauthState.csrfToken);

  if (tokenBuffer.length !== stateBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(tokenBuffer, stateBuffer);
}
