/**
 * MinIO (S3-compatible) 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 MinioProvider extends BasePlatformServiceProvider {
  readonly type: TPlatformServiceType = 'minio';
  readonly displayName = 'S3 Storage (MinIO)';
  readonly resourceTypes: TPlatformResourceType[] = ['bucket'];

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

  getDefaultConfig(): IPlatformServiceConfig {
    return {
      image: 'minio/minio:latest',
      port: 9000,
      volumes: [`${this.getPlatformDataDir('minio')}:/data`],
      command: 'server /data --console-address :9001',
      environment: {
        MINIO_ROOT_USER: 'admin',
        // Password will be generated and stored encrypted
      },
    };
  }

  getEnvVarMappings(): IEnvVarMapping[] {
    return [
      { envVar: 'S3_ENDPOINT', credentialPath: 'endpoint' },
      { envVar: 'S3_BUCKET', credentialPath: 'bucket' },
      { envVar: 'S3_ACCESS_KEY', credentialPath: 'accessKey' },
      { envVar: 'S3_SECRET_KEY', credentialPath: 'secretKey' },
      { envVar: 'S3_REGION', credentialPath: 'region' },
      // AWS SDK compatible names
      { envVar: 'AWS_ACCESS_KEY_ID', credentialPath: 'accessKey' },
      { envVar: 'AWS_SECRET_ACCESS_KEY', credentialPath: 'secretKey' },
      { envVar: 'AWS_ENDPOINT_URL', credentialPath: 'endpoint' },
      { envVar: 'AWS_REGION', credentialPath: 'region' },
    ];
  }

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

    logger.info(`Deploying MinIO 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 MinIO data
    // MinIO creates .minio.sys directory on first startup
    try {
      const stat = await Deno.stat(`${dataDir}/.minio.sys`);
      dataExists = stat.isDirectory;
      logger.info(`MinIO data directory exists with .minio.sys folder`);
    } catch {
      // .minio.sys doesn't exist, this is a fresh install
      dataExists = false;
    }

    if (dataExists && platformService?.adminCredentialsEncrypted) {
      // Reuse existing credentials from database
      logger.info('Reusing existing MinIO 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 MinIO 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('MinIO data exists but no credentials in database - wiping data directory');
        try {
          await Deno.remove(dataDir, { recursive: true });
        } catch (e) {
          logger.error(`Failed to wipe MinIO data directory: ${getErrorMessage(e)}`);
          throw new Error('Cannot deploy MinIO: 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 MinIO data directory: ${getErrorMessage(e)}`);
      }
    }

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

    const containerId = await this.oneboxRef.docker.createPlatformContainer({
      name: containerName,
      image: config.image,
      port: config.port,
      env: envVars,
      volumes: config.volumes,
      network: this.getNetworkName(),
      command: config.command?.split(' '),
      exposePorts: [9000, 9001], // API and Console ports
    });

    // 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(`MinIO container created: ${containerId}`);
    return containerId;
  }

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

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

      logger.info(`MinIO health check: using container ID ${platformService.containerId.substring(0, 12)}...`);

      // 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,
        ['curl', '-sf', 'http://localhost:9000/minio/health/live']
      );

      if (result.exitCode === 0) {
        logger.info('MinIO health check: success');
        return true;
      } else {
        logger.info(`MinIO health check failed: exit code ${result.exitCode}, stderr: ${result.stderr.substring(0, 200)}`);
        return false;
      }
    } catch (error) {
      logger.info(`MinIO 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('MinIO platform service not found or not configured');
    }

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

    // Generate bucket name
    const bucketName = this.generateBucketName(userService.name);

    logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`);

    // Use docker exec with mc (MinIO Client) inside the container
    // First configure mc alias for local server
    await this.execMc(platformService.containerId, [
      'alias', 'set', 'local', 'http://localhost:9000',
      adminCreds.username, adminCreds.password,
    ]);

    // Create the bucket
    const mbResult = await this.execMc(platformService.containerId, [
      'mb', '--ignore-existing', `local/${bucketName}`,
    ]);
    logger.info(`Created MinIO bucket '${bucketName}'`);

    // Set bucket policy to allow public read/write (services on the same network use root creds)
    await this.execMc(platformService.containerId, [
      'anonymous', 'set', 'none', `local/${bucketName}`,
    ]);

    // Use container name for the endpoint in credentials (user services run in same network)
    const serviceEndpoint = `http://${containerName}:9000`;

    const credentials: Record<string, string> = {
      endpoint: serviceEndpoint,
      bucket: bucketName,
      accessKey: adminCreds.username,
      secretKey: adminCreds.password,
      region: 'us-east-1',
    };

    // 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];
      }
    }

    logger.success(`MinIO bucket '${bucketName}' provisioned`);

    return {
      type: 'bucket',
      name: bucketName,
      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('MinIO platform service not found or not configured');
    }

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

    logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`);

    // Configure mc alias
    await this.execMc(platformService.containerId, [
      'alias', 'set', 'local', 'http://localhost:9000',
      adminCreds.username, adminCreds.password,
    ]);

    try {
      // Remove all objects and the bucket
      await this.execMc(platformService.containerId, [
        'rb', '--force', `local/${resource.resourceName}`,
      ]);
      logger.success(`MinIO bucket '${resource.resourceName}' deleted`);
    } catch (e) {
      logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`);
      throw e;
    }
  }

  /**
   * Execute mc (MinIO Client) command inside the container
   */
  private async execMc(
    containerId: string,
    args: string[],
  ): Promise<{ stdout: string; stderr: string }> {
    const result = await this.oneboxRef.docker.execInContainer(containerId, ['mc', ...args]);
    if (result.exitCode !== 0) {
      throw new Error(`mc command failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
    }
    return result;
  }
}
