// ============================================================================
// OrbiQuant IA - OAuth Service
// ============================================================================

import { OAuth2Client } from 'google-auth-library';
import axios from 'axios';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { config } from '../../../config';
import { db } from '../../../shared/database';
import { tokenService } from './token.service';
import { logger } from '../../../shared/utils/logger';
import type {
  AuthProvider,
  OAuthAccount,
  OAuthCallbackData,
  User,
  Profile,
  AuthResponse,
} from '../types/auth.types';

export class OAuthService {
  private googleClient: OAuth2Client;

  constructor() {
    this.googleClient = new OAuth2Client(
      config.oauth.google.clientId,
      config.oauth.google.clientSecret,
      config.oauth.google.callbackUrl
    );
  }

  // ============================================================================
  // Google OAuth
  // ============================================================================

  getGoogleAuthUrl(state: string): string {
    return this.googleClient.generateAuthUrl({
      access_type: 'offline',
      scope: config.oauth.google.scope,
      state,
      prompt: 'consent',
    });
  }

  async verifyGoogleToken(code: string): Promise<OAuthCallbackData | null> {
    try {
      const { tokens } = await this.googleClient.getToken(code);
      this.googleClient.setCredentials(tokens);

      const ticket = await this.googleClient.verifyIdToken({
        idToken: tokens.id_token!,
        audience: config.oauth.google.clientId,
      });

      const payload = ticket.getPayload();
      if (!payload) return null;

      return {
        provider: 'google',
        providerAccountId: payload.sub,
        email: payload.email,
        name: payload.name,
        avatarUrl: payload.picture,
        accessToken: tokens.access_token!,
        refreshToken: tokens.refresh_token ?? undefined,
        expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : undefined,
        profile: payload as unknown as Record<string, unknown>,
      };
    } catch (error) {
      logger.error('Google token verification failed', { error });
      return null;
    }
  }

  async verifyGoogleIdToken(idToken: string): Promise<OAuthCallbackData | null> {
    try {
      const ticket = await this.googleClient.verifyIdToken({
        idToken,
        audience: config.oauth.google.clientId,
      });

      const payload = ticket.getPayload();
      if (!payload) return null;

      return {
        provider: 'google',
        providerAccountId: payload.sub,
        email: payload.email,
        name: payload.name,
        avatarUrl: payload.picture,
        accessToken: idToken,
        profile: payload as unknown as Record<string, unknown>,
      };
    } catch (error) {
      logger.error('Google ID token verification failed', { error });
      return null;
    }
  }

  // ============================================================================
  // Facebook OAuth
  // ============================================================================

  getFacebookAuthUrl(state: string): string {
    const params = new URLSearchParams({
      client_id: config.oauth.facebook.clientId,
      redirect_uri: config.oauth.facebook.callbackUrl,
      scope: config.oauth.facebook.scope.join(','),
      state,
      response_type: 'code',
    });

    return `https://www.facebook.com/v18.0/dialog/oauth?${params}`;
  }

  async verifyFacebookToken(code: string): Promise<OAuthCallbackData | null> {
    try {
      // Exchange code for access token
      const tokenResponse = await axios.get('https://graph.facebook.com/v18.0/oauth/access_token', {
        params: {
          client_id: config.oauth.facebook.clientId,
          client_secret: config.oauth.facebook.clientSecret,
          redirect_uri: config.oauth.facebook.callbackUrl,
          code,
        },
      });

      const { access_token, expires_in } = tokenResponse.data;

      // Get user profile
      const profileResponse = await axios.get('https://graph.facebook.com/v18.0/me', {
        params: {
          fields: 'id,name,email,picture.type(large)',
          access_token,
        },
      });

      const profile = profileResponse.data;

      return {
        provider: 'facebook',
        providerAccountId: profile.id,
        email: profile.email,
        name: profile.name,
        avatarUrl: profile.picture?.data?.url,
        accessToken: access_token,
        expiresAt: expires_in ? new Date(Date.now() + expires_in * 1000) : undefined,
        profile,
      };
    } catch (error) {
      logger.error('Facebook token verification failed', { error });
      return null;
    }
  }

  // ============================================================================
  // Twitter/X OAuth 2.0
  // ============================================================================

  getTwitterAuthUrl(state: string, codeChallenge: string): string {
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: config.oauth.twitter.clientId,
      redirect_uri: config.oauth.twitter.callbackUrl,
      scope: config.oauth.twitter.scope.join(' '),
      state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
    });

    return `https://twitter.com/i/oauth2/authorize?${params}`;
  }

  async verifyTwitterToken(code: string, codeVerifier: string): Promise<OAuthCallbackData | null> {
    try {
      // Exchange code for access token
      const tokenResponse = await axios.post(
        'https://api.twitter.com/2/oauth2/token',
        new URLSearchParams({
          code,
          grant_type: 'authorization_code',
          client_id: config.oauth.twitter.clientId,
          redirect_uri: config.oauth.twitter.callbackUrl,
          code_verifier: codeVerifier,
        }),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            Authorization: `Basic ${Buffer.from(
              `${config.oauth.twitter.clientId}:${config.oauth.twitter.clientSecret}`
            ).toString('base64')}`,
          },
        }
      );

      const { access_token, refresh_token, expires_in } = tokenResponse.data;

      // Get user profile
      const profileResponse = await axios.get('https://api.twitter.com/2/users/me', {
        params: {
          'user.fields': 'id,name,username,profile_image_url',
        },
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      });

      const profile = profileResponse.data.data;

      return {
        provider: 'twitter',
        providerAccountId: profile.id,
        name: profile.name,
        avatarUrl: profile.profile_image_url,
        accessToken: access_token,
        refreshToken: refresh_token,
        expiresAt: expires_in ? new Date(Date.now() + expires_in * 1000) : undefined,
        profile,
      };
    } catch (error) {
      logger.error('Twitter token verification failed', { error });
      return null;
    }
  }

  // ============================================================================
  // Apple Sign In
  // ============================================================================

  getAppleAuthUrl(state: string): string {
    const params = new URLSearchParams({
      client_id: config.oauth.apple.clientId,
      redirect_uri: config.oauth.apple.callbackUrl,
      response_type: 'code id_token',
      scope: config.oauth.apple.scope.join(' '),
      response_mode: 'form_post',
      state,
    });

    return `https://appleid.apple.com/auth/authorize?${params}`;
  }

  async verifyAppleToken(code: string, idToken: string): Promise<OAuthCallbackData | null> {
    try {
      // Verify ID token
      const decodedHeader = jwt.decode(idToken, { complete: true });
      if (!decodedHeader) return null;

      // Get Apple's public keys
      const keysResponse = await axios.get('https://appleid.apple.com/auth/keys');
      const keys = keysResponse.data.keys;

      // Find matching key
      const key = keys.find((k: { kid: string }) => k.kid === decodedHeader.header.kid);
      if (!key) return null;

      // Verify token (simplified - in production use proper JWT verification)
      const decoded = jwt.decode(idToken) as {
        sub: string;
        email?: string;
        email_verified?: boolean;
      } | null;

      if (!decoded) return null;

      return {
        provider: 'apple',
        providerAccountId: decoded.sub,
        email: decoded.email,
        accessToken: code,
        profile: decoded as unknown as Record<string, unknown>,
      };
    } catch (error) {
      logger.error('Apple token verification failed', { error });
      return null;
    }
  }

  // ============================================================================
  // GitHub OAuth
  // ============================================================================

  getGitHubAuthUrl(state: string): string {
    const params = new URLSearchParams({
      client_id: config.oauth.github.clientId,
      redirect_uri: config.oauth.github.callbackUrl,
      scope: config.oauth.github.scope.join(' '),
      state,
    });

    return `https://github.com/login/oauth/authorize?${params}`;
  }

  async verifyGitHubToken(code: string): Promise<OAuthCallbackData | null> {
    try {
      // Exchange code for access token
      const tokenResponse = await axios.post(
        'https://github.com/login/oauth/access_token',
        {
          client_id: config.oauth.github.clientId,
          client_secret: config.oauth.github.clientSecret,
          code,
          redirect_uri: config.oauth.github.callbackUrl,
        },
        {
          headers: {
            Accept: 'application/json',
          },
        }
      );

      const { access_token } = tokenResponse.data;

      // Get user profile
      const profileResponse = await axios.get('https://api.github.com/user', {
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
      });

      const profile = profileResponse.data;

      // Get user email if not public
      let email = profile.email;
      if (!email) {
        const emailsResponse = await axios.get('https://api.github.com/user/emails', {
          headers: {
            Authorization: `Bearer ${access_token}`,
          },
        });
        const primaryEmail = emailsResponse.data.find(
          (e: { primary: boolean; verified: boolean }) => e.primary && e.verified
        );
        email = primaryEmail?.email;
      }

      return {
        provider: 'github',
        providerAccountId: profile.id.toString(),
        email,
        name: profile.name || profile.login,
        avatarUrl: profile.avatar_url,
        accessToken: access_token,
        profile,
      };
    } catch (error) {
      logger.error('GitHub token verification failed', { error });
      return null;
    }
  }

  // ============================================================================
  // Common OAuth Flow
  // ============================================================================

  async handleOAuthCallback(
    data: OAuthCallbackData,
    userAgent?: string,
    ipAddress?: string
  ): Promise<AuthResponse> {
    // Check if OAuth account exists
    const existingOAuth = await db.query<OAuthAccount>(
      `SELECT * FROM oauth_accounts
       WHERE provider = $1 AND provider_account_id = $2`,
      [data.provider, data.providerAccountId]
    );

    let user: User;
    let profile: Profile | undefined;
    let isNewUser = false;

    if (existingOAuth.rows.length > 0) {
      // Existing OAuth account - get user
      const userResult = await db.query<User>(
        'SELECT * FROM users WHERE id = $1',
        [existingOAuth.rows[0].userId]
      );
      user = userResult.rows[0];

      // Update OAuth tokens
      await db.query(
        `UPDATE oauth_accounts
         SET access_token = $1, refresh_token = $2, token_expires_at = $3, updated_at = NOW()
         WHERE id = $4`,
        [data.accessToken, data.refreshToken, data.expiresAt, existingOAuth.rows[0].id]
      );

      // Get profile
      const profileResult = await db.query<Profile>(
        'SELECT * FROM profiles WHERE user_id = $1',
        [user.id]
      );
      profile = profileResult.rows[0];
    } else if (data.email) {
      // Check if user with this email exists
      const existingUser = await db.query<User>(
        'SELECT * FROM users WHERE email = $1',
        [data.email]
      );

      if (existingUser.rows.length > 0) {
        // Link OAuth to existing user
        user = existingUser.rows[0];

        await db.query(
          `INSERT INTO oauth_accounts
           (user_id, provider, provider_account_id, access_token, refresh_token, token_expires_at, provider_email, provider_name, provider_avatar_url, provider_profile)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
          [
            user.id,
            data.provider,
            data.providerAccountId,
            data.accessToken,
            data.refreshToken,
            data.expiresAt,
            data.email,
            data.name,
            data.avatarUrl,
            JSON.stringify(data.profile),
          ]
        );

        const profileResult = await db.query<Profile>(
          'SELECT * FROM profiles WHERE user_id = $1',
          [user.id]
        );
        profile = profileResult.rows[0];
      } else {
        // Create new user
        isNewUser = true;
        const result = await this.createUserFromOAuth(data);
        user = result.user;
        profile = result.profile;
      }
    } else {
      // No email - create user without email (for Twitter/Apple without email)
      isNewUser = true;
      const result = await this.createUserFromOAuth(data);
      user = result.user;
      profile = result.profile;
    }

    // Update last login
    await db.query(
      `UPDATE users SET last_login_at = NOW(), last_login_ip = $1 WHERE id = $2`,
      [ipAddress, user.id]
    );

    // Log auth event
    await db.query(
      `INSERT INTO auth_logs (user_id, event, provider, ip_address, user_agent, success)
       VALUES ($1, $2, $3, $4, $5, true)`,
      [user.id, isNewUser ? 'register' : 'login_success', data.provider, ipAddress, userAgent]
    );

    // Create session and tokens
    const { tokens } = await tokenService.createSession(user.id, userAgent, ipAddress);

    // Remove sensitive data
    const { encryptedPassword: _, ...safeUser } = user;

    return {
      user: safeUser as Omit<User, 'encryptedPassword'>,
      profile,
      tokens,
      isNewUser,
    };
  }

  private async createUserFromOAuth(data: OAuthCallbackData): Promise<{ user: User; profile: Profile }> {
    const client = await db.getClient();

    try {
      await client.query('BEGIN');

      // Create user
      const userResult = await client.query<User>(
        `INSERT INTO users (email, email_verified, primary_auth_provider, status)
         VALUES ($1, $2, $3, 'active')
         RETURNING *`,
        [data.email || `${data.provider}_${data.providerAccountId}@oauth.orbiquant.com`, true, data.provider]
      );
      const user = userResult.rows[0];

      // Create profile
      const names = data.name?.split(' ') || [];
      const profileResult = await client.query<Profile>(
        `INSERT INTO profiles (user_id, first_name, last_name, display_name, avatar_url)
         VALUES ($1, $2, $3, $4, $5)
         RETURNING *`,
        [user.id, names[0] || null, names.slice(1).join(' ') || null, data.name, data.avatarUrl]
      );
      const profile = profileResult.rows[0];

      // Create user settings
      await client.query(
        'INSERT INTO user_settings (user_id) VALUES ($1)',
        [user.id]
      );

      // Create OAuth account
      await client.query(
        `INSERT INTO oauth_accounts
         (user_id, provider, provider_account_id, access_token, refresh_token, token_expires_at, provider_email, provider_name, provider_avatar_url, provider_profile)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
        [
          user.id,
          data.provider,
          data.providerAccountId,
          data.accessToken,
          data.refreshToken,
          data.expiresAt,
          data.email,
          data.name,
          data.avatarUrl,
          JSON.stringify(data.profile),
        ]
      );

      await client.query('COMMIT');

      return { user, profile };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async linkOAuthAccount(userId: string, data: OAuthCallbackData): Promise<OAuthAccount> {
    // Check if already linked
    const existing = await db.query(
      'SELECT * FROM oauth_accounts WHERE user_id = $1 AND provider = $2',
      [userId, data.provider]
    );

    if (existing.rows.length > 0) {
      throw new Error(`${data.provider} account already linked`);
    }

    // Check if OAuth account is linked to another user
    const otherUser = await db.query(
      'SELECT * FROM oauth_accounts WHERE provider = $1 AND provider_account_id = $2',
      [data.provider, data.providerAccountId]
    );

    if (otherUser.rows.length > 0) {
      throw new Error(`This ${data.provider} account is already linked to another user`);
    }

    const result = await db.query<OAuthAccount>(
      `INSERT INTO oauth_accounts
       (user_id, provider, provider_account_id, access_token, refresh_token, token_expires_at, provider_email, provider_name, provider_avatar_url, provider_profile)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
       RETURNING *`,
      [
        userId,
        data.provider,
        data.providerAccountId,
        data.accessToken,
        data.refreshToken,
        data.expiresAt,
        data.email,
        data.name,
        data.avatarUrl,
        JSON.stringify(data.profile),
      ]
    );

    return result.rows[0];
  }

  async unlinkOAuthAccount(userId: string, provider: AuthProvider): Promise<void> {
    // Check if user has other auth methods
    const user = await db.query<User>(
      'SELECT * FROM users WHERE id = $1',
      [userId]
    );

    const oauthAccounts = await db.query(
      'SELECT * FROM oauth_accounts WHERE user_id = $1',
      [userId]
    );

    const hasPassword = !!user.rows[0].encryptedPassword;
    const hasOtherOAuth = oauthAccounts.rows.length > 1;

    if (!hasPassword && !hasOtherOAuth) {
      throw new Error('Cannot unlink the only authentication method');
    }

    await db.query(
      'DELETE FROM oauth_accounts WHERE user_id = $1 AND provider = $2',
      [userId, provider]
    );
  }

  async getLinkedAccounts(userId: string): Promise<OAuthAccount[]> {
    const result = await db.query<OAuthAccount>(
      `SELECT id, user_id, provider, provider_account_id, provider_email, provider_name, provider_avatar_url, created_at
       FROM oauth_accounts WHERE user_id = $1`,
      [userId]
    );

    return result.rows;
  }

  // PKCE helpers for Twitter
  generateCodeVerifier(): string {
    return crypto.randomBytes(32).toString('base64url');
  }

  generateCodeChallenge(verifier: string): string {
    return crypto.createHash('sha256').update(verifier).digest('base64url');
  }

  generateState(): string {
    return crypto.randomBytes(16).toString('hex');
  }
}

export const oauthService = new OAuthService();
