import { Pool } from 'pg';
import { DatabaseManager } from '@/infra/database/database.manager.js';
import { SecretService } from '@/services/secrets/secret.service.js';
import { AppError } from '@/utils/errors.js';
import logger from '@/utils/logger.js';
import { ERROR_CODES, OAuthConfigSchema, OAuthProvidersSchema } from '@insforge/shared-schemas';

export interface CreateOAuthConfigInput {
  provider: OAuthProvidersSchema;
  clientId?: string;
  clientSecret?: string;
  redirectUri?: string;
  scopes?: string[];
  useSharedKey?: boolean;
}

export interface UpdateOAuthConfigInput {
  clientId?: string;
  clientSecret?: string;
  redirectUri?: string;
  scopes?: string[];
  useSharedKey?: boolean;
}

export class OAuthConfigService {
  private static instance: OAuthConfigService;
  private pool: Pool | null = null;
  private secretService: SecretService;

  private constructor() {
    this.secretService = SecretService.getInstance();
    logger.info('OAuthConfigService initialized');
  }

  public static getInstance(): OAuthConfigService {
    if (!OAuthConfigService.instance) {
      OAuthConfigService.instance = new OAuthConfigService();
    }
    return OAuthConfigService.instance;
  }

  private getPool(): Pool {
    if (!this.pool) {
      this.pool = DatabaseManager.getInstance().getPool();
    }
    return this.pool;
  }

  /**
   * Get all OAuth configurations
   */
  async getAllConfigs(): Promise<OAuthConfigSchema[]> {
    try {
      const result = await this.getPool().query(
        `SELECT
          id,
          provider,
          client_id as "clientId",
          redirect_uri as "redirectUri",
          scopes,
          use_shared_key as "useSharedKey",
          created_at as "createdAt",
          updated_at as "updatedAt"
         FROM auth.oauth_configs
         ORDER BY provider ASC`
      );

      return result.rows;
    } catch (error) {
      logger.error('Failed to get OAuth configs', { error });
      throw new AppError('Failed to get OAuth configurations', 500, ERROR_CODES.INTERNAL_ERROR);
    }
  }

  /**
   * Get public OAuth provider information (safe for public API)
   * Only returns non-sensitive information about configured providers
   */
  async getConfiguredProviders(): Promise<OAuthProvidersSchema[]> {
    try {
      const result = await this.getPool().query(
        `SELECT
          provider
         FROM auth.oauth_configs
         ORDER BY provider ASC`
      );

      return result.rows.map((row) => row.provider);
    } catch (error) {
      logger.error('Failed to get public OAuth providers', { error });
      throw new AppError('Failed to get OAuth providers', 500, ERROR_CODES.INTERNAL_ERROR);
    }
  }

  /**
   * Get OAuth configuration by provider name
   */
  async getConfigByProvider(provider: string): Promise<OAuthConfigSchema | null> {
    try {
      const result = await this.getPool().query(
        `SELECT
          id,
          provider,
          client_id as "clientId",
          redirect_uri as "redirectUri",
          scopes,
          use_shared_key as "useSharedKey",
          created_at as "createdAt",
          updated_at as "updatedAt"
         FROM auth.oauth_configs
         WHERE LOWER(provider) = LOWER($1)
         LIMIT 1`,
        [provider]
      );

      if (!result.rows.length) {
        return null;
      }

      return result.rows[0];
    } catch (error) {
      logger.error('Failed to get OAuth config by provider', { error, provider });
      throw new AppError('Failed to get OAuth configuration', 500, ERROR_CODES.INTERNAL_ERROR);
    }
  }

  /**
   * Get OAuth provider secret
   */
  async getClientSecretByProvider(provider: string): Promise<string | null> {
    try {
      const result = await this.getPool().query(
        `SELECT
          secret_id as "secretId"
         FROM auth.oauth_configs
         WHERE LOWER(provider) = LOWER($1)
         LIMIT 1`,
        [provider]
      );

      if (!result.rows.length) {
        return null;
      }

      const config = result.rows[0];
      const clientSecret = await this.secretService.getSecretById(config.secretId);
      if (!clientSecret) {
        logger.warn('OAuth config exists but secret not found', { provider });
        return null;
      }

      return clientSecret;
    } catch (error) {
      logger.error('Failed to get OAuth config with secret', { error, provider });
      throw new AppError('Failed to get OAuth configuration', 500, ERROR_CODES.INTERNAL_ERROR);
    }
  }

  /**
   * Create OAuth configuration
   */
  async createConfig(input: CreateOAuthConfigInput): Promise<OAuthConfigSchema> {
    const client = await this.getPool().connect();
    try {
      await client.query('BEGIN');

      // Check if config already exists for this provider
      const existingConfig = await client.query(
        'SELECT id FROM auth.oauth_configs WHERE LOWER(provider) = LOWER($1)',
        [input.provider]
      );

      if (existingConfig.rows.length) {
        throw new AppError(
          `OAuth configuration for ${input.provider} already exists`,
          409,
          ERROR_CODES.AUTH_OAUTH_CONFIG_ALREADY_EXISTS
        );
      }

      let secretId: string | null = null;

      // Only create secret if clientSecret is provided and not using shared key
      if (input.clientSecret && !input.useSharedKey) {
        // Create new secret
        const secret = await this.secretService.createSecret(
          {
            key: `${input.provider.toUpperCase()}_CLIENT_SECRET`,
            value: input.clientSecret,
          },
          client
        );
        secretId = secret.id;
      }

      // Set default scopes if not provided
      let scopes = input.scopes;
      if (!scopes) {
        switch (input.provider) {
          case 'google':
            scopes = ['openid', 'email', 'profile'];
            break;
          case 'github':
            scopes = ['user:email'];
            break;
          case 'discord':
            scopes = ['identify', 'email'];
            break;
          case 'linkedin':
            scopes = ['openid', 'profile', 'email'];
            break;
          case 'facebook':
            scopes = ['email', 'public_profile'];
            break;
          case 'instagram':
            scopes = ['user_profile', 'user_media'];
            break;
          case 'tiktok':
            scopes = ['user.info.basic'];
            break;
          case 'apple':
            scopes = ['name', 'email'];
            break;
          case 'x':
            scopes = ['tweet.read', 'users.read'];
            break;
          case 'spotify':
            scopes = ['user-read-email', 'user-read-private'];
            break;
          case 'microsoft':
            scopes = ['User.Read'];
            break;
          default:
            scopes = ['email', 'profile'];
        }
      }

      // Create new OAuth config
      const result = await client.query(
        `INSERT INTO auth.oauth_configs (provider, client_id, secret_id, redirect_uri, scopes, use_shared_key)
         VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING
           id,
           provider,
           client_id as "clientId",
           redirect_uri as "redirectUri",
           scopes,
           use_shared_key as "useSharedKey",
           created_at as "createdAt",
           updated_at as "updatedAt"`,
        [
          input.provider.toLowerCase(),
          input.clientId || null,
          secretId,
          null, // Deprecating redirect_uri
          scopes,
          input.useSharedKey || false,
        ]
      );

      await client.query('COMMIT');
      logger.info('OAuth config created', { provider: input.provider });

      return result.rows[0];
    } catch (error) {
      await client.query('ROLLBACK');
      logger.error('Failed to create OAuth config', { error, provider: input.provider });
      if (error instanceof AppError) {
        throw error;
      }
      throw new AppError('Failed to create OAuth configuration', 500, ERROR_CODES.INTERNAL_ERROR);
    } finally {
      client.release();
    }
  }

  /**
   * Update OAuth configuration
   */
  async updateConfig(provider: string, input: UpdateOAuthConfigInput): Promise<OAuthConfigSchema> {
    const client = await this.getPool().connect();
    try {
      await client.query('BEGIN');

      // Get existing config with secret_id
      const existingResult = await client.query(
        `SELECT id, secret_id as "secretId" FROM auth.oauth_configs WHERE LOWER(provider) = LOWER($1)`,
        [provider]
      );

      if (!existingResult.rows.length) {
        throw new AppError(
          'OAuth configuration not found',
          404,
          ERROR_CODES.AUTH_OAUTH_CONFIG_NOT_FOUND
        );
      }

      const existingConfig = existingResult.rows[0];

      // Update or create secret if provided
      if (input.clientSecret !== undefined) {
        if (existingConfig.secretId) {
          // Update existing secret
          await this.secretService.updateSecret(
            existingConfig.secretId,
            {
              value: input.clientSecret,
            },
            client
          );
        } else {
          // Create new secret if it doesn't exist
          const secret = await this.secretService.createSecret(
            {
              key: `${provider.toUpperCase()}_CLIENT_SECRET`,
              value: input.clientSecret,
            },
            client
          );
          // Add secret_id to the update query
          await client.query(`UPDATE auth.oauth_configs SET secret_id = $1 WHERE id = $2`, [
            secret.id,
            existingConfig.id,
          ]);
        }
      }

      // Build update query
      const updates: string[] = [];
      const values: (string | string[] | boolean | null)[] = [];
      let paramCount = 1;

      if (input.clientId !== undefined) {
        updates.push(`client_id = $${paramCount++}`);
        values.push(input.clientId);
      }

      if (input.redirectUri !== undefined) {
        updates.push(`redirect_uri = $${paramCount++}`);
        values.push(input.redirectUri);
      }

      if (input.scopes !== undefined) {
        updates.push(`scopes = $${paramCount++}`);
        values.push(input.scopes);
      }

      if (input.useSharedKey !== undefined) {
        updates.push(`use_shared_key = $${paramCount++}`);
        values.push(input.useSharedKey);
      }

      if (!updates.length && input.clientSecret === undefined) {
        await client.query('COMMIT');
        // Return the config in the correct format
        const config = await this.getConfigByProvider(provider);
        if (!config) {
          throw new AppError('Failed to retrieve configuration', 500, ERROR_CODES.INTERNAL_ERROR);
        }
        return config;
      }

      if (updates.length) {
        updates.push('updated_at = NOW()');
        values.push(provider.toLowerCase());

        const result = await client.query(
          `UPDATE auth.oauth_configs
           SET ${updates.join(', ')}
           WHERE LOWER(provider) = $${paramCount}
           RETURNING
             id,
             provider,
             client_id as "clientId",
             redirect_uri as "redirectUri",
             scopes,
             use_shared_key as "useSharedKey",
             created_at as "createdAt",
             updated_at as "updatedAt"`,
          values
        );

        await client.query('COMMIT');
        logger.info('OAuth config updated', { provider });
        return result.rows[0];
      }

      // Only secret was updated
      await client.query('COMMIT');
      const updatedConfig = await this.getConfigByProvider(provider);
      if (!updatedConfig) {
        throw new AppError(
          'Failed to retrieve updated configuration',
          500,
          ERROR_CODES.INTERNAL_ERROR
        );
      }
      return updatedConfig;
    } catch (error) {
      await client.query('ROLLBACK');
      logger.error('Failed to update OAuth config', { error, provider });
      if (error instanceof AppError) {
        throw error;
      }
      throw new AppError('Failed to update OAuth configuration', 500, ERROR_CODES.INTERNAL_ERROR);
    } finally {
      client.release();
    }
  }

  /**
   * Delete OAuth configuration
   */
  async deleteConfig(provider: string): Promise<boolean> {
    const client = await this.getPool().connect();
    try {
      await client.query('BEGIN');

      // Get existing config with secret_id
      const existingResult = await client.query(
        `SELECT id, secret_id as "secretId" FROM auth.oauth_configs WHERE LOWER(provider) = LOWER($1)`,
        [provider]
      );

      if (!existingResult.rows.length) {
        await client.query('ROLLBACK');
        return false;
      }

      const existingConfig = existingResult.rows[0];

      // Delete OAuth config (secret will be restricted due to foreign key)
      const result = await client.query(
        'DELETE FROM auth.oauth_configs WHERE LOWER(provider) = LOWER($1)',
        [provider]
      );

      // Try to delete the associated secret (will fail if still referenced)
      try {
        const deletedSecret = await this.secretService.deleteSecret(
          existingConfig.secretId,
          client
        );
        if (deletedSecret) {
          logger.info('Associated secret deleted', { secretId: existingConfig.secretId });
        } else {
          logger.warn('Could not delete associated secret, it may be in use elsewhere', {
            provider,
            secretId: existingConfig.secretId,
          });
        }
      } catch {
        logger.warn('Could not delete associated secret, it may be in use elsewhere', {
          provider,
          secretId: existingConfig.secretId,
        });
      }

      await client.query('COMMIT');

      const success = (result.rowCount ?? 0) > 0;
      if (success) {
        logger.info('OAuth config deleted', { provider });
      }
      return success;
    } catch (error) {
      await client.query('ROLLBACK');
      logger.error('Failed to delete OAuth config', { error, provider });
      throw new AppError('Failed to delete OAuth configuration', 500, ERROR_CODES.INTERNAL_ERROR);
    } finally {
      client.release();
    }
  }
}
