/**
 * MongoDB Platform Service Provider
 */

import { BasePlatformServiceProvider } from './base.ts';
import type {
  IService,
  IPlatformResource,
  IPlatformServiceConfig,
  IProvisionedResource,
  IEnvVarMapping,
  TPlatformServiceType,
  TPlatformResourceType,
} from '../../../types.ts';
import { logger } from '../../../logging.ts';
import { getErrorMessage } from '../../../utils/error.ts';
import { credentialEncryption } from '../../encryption.ts';
import type { Onebox } from '../../onebox.ts';

export class MongoDBProvider extends BasePlatformServiceProvider {
  readonly type: TPlatformServiceType = 'mongodb';
  readonly displayName = 'MongoDB';
  readonly resourceTypes: TPlatformResourceType[] = ['database'];

  constructor(oneboxRef: Onebox) {
    super(oneboxRef);
  }

  getDefaultConfig(): IPlatformServiceConfig {
    return {
      image: 'mongo:4.4',
      port: 27017,
      volumes: [`${this.getPlatformDataDir('mongodb')}:/data/db`],
      environment: {
        MONGO_INITDB_ROOT_USERNAME: 'admin',
        // Password will be generated and stored encrypted
      },
    };
  }

  getEnvVarMappings(): IEnvVarMapping[] {
    return [
      { envVar: 'MONGODB_URI', credentialPath: 'connectionString' },
      { envVar: 'MONGODB_HOST', credentialPath: 'host' },
      { envVar: 'MONGODB_PORT', credentialPath: 'port' },
      { envVar: 'MONGODB_DATABASE', credentialPath: 'database' },
      { envVar: 'MONGODB_USERNAME', credentialPath: 'username' },
      { envVar: 'MONGODB_PASSWORD', credentialPath: 'password' },
    ];
  }

  async deployContainer(): Promise<string> {
    const config = this.getDefaultConfig();
    const containerName = this.getContainerName();
    const dataDir = this.getPlatformDataDir('mongodb');

    logger.info(`Deploying MongoDB platform service as ${containerName}...`);

    // Check if we have existing data and stored credentials
    const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
    let adminCredentials: { username: string; password: string };
    let dataExists = false;

    // Check if data directory has existing MongoDB data
    try {
      const stat = await Deno.stat(`${dataDir}/WiredTiger`);
      dataExists = stat.isFile;
      logger.info(`MongoDB data directory exists with WiredTiger file`);
    } catch {
      // WiredTiger file doesn't exist, this is a fresh install
      dataExists = false;
    }

    if (dataExists && platformService?.adminCredentialsEncrypted) {
      // Reuse existing credentials from database
      logger.info('Reusing existing MongoDB credentials (data directory already initialized)');
      adminCredentials = await credentialEncryption.decrypt<{ username: string; password: string }>(
        platformService.adminCredentialsEncrypted,
      );
    } else {
      // Generate new credentials for fresh deployment
      logger.info('Generating new MongoDB admin credentials');
      adminCredentials = {
        username: 'admin',
        password: credentialEncryption.generatePassword(32),
      };

      // If data exists but we don't have credentials, we need to wipe the data
      if (dataExists) {
        logger.warn('MongoDB data exists but no credentials in database - wiping data directory');
        try {
          await Deno.remove(dataDir, { recursive: true });
        } catch (e) {
          logger.error(`Failed to wipe MongoDB data directory: ${getErrorMessage(e)}`);
          throw new Error('Cannot deploy MongoDB: data directory exists without credentials');
        }
      }
    }

    // Ensure data directory exists
    try {
      await Deno.mkdir(dataDir, { recursive: true });
    } catch (e) {
      // Directory might already exist
      if (!(e instanceof Deno.errors.AlreadyExists)) {
        logger.warn(`Could not create MongoDB data directory: ${getErrorMessage(e)}`);
      }
    }

    // Create container using Docker API
    const envVars = [
      `MONGO_INITDB_ROOT_USERNAME=${adminCredentials.username}`,
      `MONGO_INITDB_ROOT_PASSWORD=${adminCredentials.password}`,
    ];

    // Use Docker to create the container
    const containerId = await this.oneboxRef.docker.createPlatformContainer({
      name: containerName,
      image: config.image,
      port: config.port,
      env: envVars,
      volumes: config.volumes,
      network: this.getNetworkName(),
    });

    // Store encrypted admin credentials (only update if new or changed)
    const encryptedCreds = await credentialEncryption.encrypt(adminCredentials);
    if (platformService) {
      this.oneboxRef.database.updatePlatformService(platformService.id!, {
        containerId,
        adminCredentialsEncrypted: encryptedCreds,
        status: 'starting',
      });
    }

    logger.success(`MongoDB container created: ${containerId}`);
    return containerId;
  }

  async stopContainer(containerId: string): Promise<void> {
    logger.info(`Stopping MongoDB container ${containerId}...`);
    await this.oneboxRef.docker.stopContainer(containerId);
    logger.success('MongoDB container stopped');
  }

  async healthCheck(): Promise<boolean> {
    try {
      logger.info('MongoDB health check: starting...');
      const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
      if (!platformService) {
        logger.info('MongoDB health check: platform service not found in database');
        return false;
      }
      if (!platformService.adminCredentialsEncrypted) {
        logger.info('MongoDB health check: no admin credentials stored');
        return false;
      }
      if (!platformService.containerId) {
        logger.info('MongoDB health check: no container ID in database record');
        return false;
      }

      logger.info(`MongoDB health check: using container ID ${platformService.containerId.substring(0, 12)}...`);
      const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);

      // Use docker exec to run health check inside the container
      // This avoids network issues with overlay networks
      const result = await this.oneboxRef.docker.execInContainer(
        platformService.containerId,
        ['mongo', '--eval', 'db.adminCommand("ping")', '--username', adminCreds.username, '--password', adminCreds.password, '--authenticationDatabase', 'admin', '--quiet']
      );

      if (result.exitCode === 0) {
        logger.info('MongoDB health check: success');
        return true;
      } else {
        logger.info(`MongoDB health check failed: exit code ${result.exitCode}, stderr: ${result.stderr.substring(0, 200)}`);
        return false;
      }
    } catch (error) {
      logger.info(`MongoDB health check exception: ${getErrorMessage(error)}`);
      return false;
    }
  }

  async provisionResource(userService: IService): Promise<IProvisionedResource> {
    const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
    if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) {
      throw new Error('MongoDB platform service not found or not configured');
    }

    const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
    const containerName = this.getContainerName();

    // Generate resource names and credentials
    const dbName = this.generateResourceName(userService.name);
    const username = this.generateResourceName(userService.name);
    const password = credentialEncryption.generatePassword(32);

    logger.info(`Provisioning MongoDB database '${dbName}' for service '${userService.name}'...`);

    // Use docker exec to provision inside the container (avoids host port mapping issues)
    const escapedPassword = password.replace(/'/g, "'\\''");
    const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");

    // Create database and user via mongo inside the container
    const mongoScript = `
      db = db.getSiblingDB('${dbName}');
      db.createCollection('_onebox_init');
      db.createUser({
        user: '${username}',
        pwd: '${escapedPassword}',
        roles: [{ role: 'readWrite', db: '${dbName}' }]
      });
      print('PROVISION_SUCCESS');
    `;

    const result = await this.oneboxRef.docker.execInContainer(
      platformService.containerId,
      [
        'mongo',
        '--username', adminCreds.username,
        '--password', escapedAdminPassword,
        '--authenticationDatabase', 'admin',
        '--quiet',
        '--eval', mongoScript,
      ]
    );

    if (result.exitCode !== 0 || !result.stdout.includes('PROVISION_SUCCESS')) {
      throw new Error(`Failed to provision MongoDB database: exit code ${result.exitCode}, output: ${result.stdout.substring(0, 200)} ${result.stderr.substring(0, 200)}`);
    }

    logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`);

    // Build the credentials and env vars
    const credentials: Record<string, string> = {
      host: containerName,
      port: '27017',
      database: dbName,
      username,
      password,
      connectionString: `mongodb://${username}:${password}@${containerName}:27017/${dbName}?authSource=${dbName}`,
    };

    // Map credentials to env vars
    const envVars: Record<string, string> = {};
    for (const mapping of this.getEnvVarMappings()) {
      if (credentials[mapping.credentialPath]) {
        envVars[mapping.envVar] = credentials[mapping.credentialPath];
      }
    }

    return {
      type: 'database',
      name: dbName,
      credentials,
      envVars,
    };
  }

  async deprovisionResource(resource: IPlatformResource, credentials: Record<string, string>): Promise<void> {
    const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
    if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) {
      throw new Error('MongoDB platform service not found or not configured');
    }

    const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
    const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");

    logger.info(`Deprovisioning MongoDB database '${resource.resourceName}'...`);

    const mongoScript = `
      db = db.getSiblingDB('${resource.resourceName}');
      try { db.dropUser('${credentials.username}'); } catch(e) { print('User drop failed: ' + e); }
      db.dropDatabase();
      print('DEPROVISION_SUCCESS');
    `;

    const result = await this.oneboxRef.docker.execInContainer(
      platformService.containerId,
      [
        'mongo',
        '--username', adminCreds.username,
        '--password', escapedAdminPassword,
        '--authenticationDatabase', 'admin',
        '--quiet',
        '--eval', mongoScript,
      ]
    );

    if (result.exitCode !== 0) {
      logger.warn(`MongoDB deprovision returned exit code ${result.exitCode}: ${result.stderr.substring(0, 200)}`);
    }

    logger.success(`MongoDB database '${resource.resourceName}' dropped`);
  }
}
