/**
 * Redis 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 RedisProvider extends BasePlatformServiceProvider {
  readonly type: TPlatformServiceType = 'redis';
  readonly displayName = 'Redis';
  readonly resourceTypes: TPlatformResourceType[] = ['cache'];

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

  getDefaultConfig(): IPlatformServiceConfig {
    return {
      image: 'redis:7-alpine',
      port: 6379,
      volumes: [`${this.getPlatformDataDir('redis')}:/data`],
      environment: {},
    };
  }

  getEnvVarMappings(): IEnvVarMapping[] {
    return [
      { envVar: 'REDIS_HOST', credentialPath: 'host' },
      { envVar: 'REDIS_PORT', credentialPath: 'port' },
      { envVar: 'REDIS_PASSWORD', credentialPath: 'password' },
      { envVar: 'REDIS_DB', credentialPath: 'db' },
      { envVar: 'REDIS_URL', credentialPath: 'connectionString' },
    ];
  }

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

    logger.info(`Deploying Redis 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 Redis data
    try {
      const stat = await Deno.stat(`${dataDir}/dump.rdb`);
      dataExists = stat.isFile;
      logger.info(`Redis data directory exists with dump.rdb file`);
    } catch {
      // Also check for appendonly file
      try {
        const stat = await Deno.stat(`${dataDir}/appendonly.aof`);
        dataExists = stat.isFile;
        logger.info(`Redis data directory exists with appendonly.aof file`);
      } catch {
        dataExists = false;
      }
    }

    if (dataExists && platformService?.adminCredentialsEncrypted) {
      // Reuse existing credentials from database
      logger.info('Reusing existing Redis 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 Redis admin credentials');
      adminCredentials = {
        username: 'default',
        password: credentialEncryption.generatePassword(32),
      };

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

    // Redis uses command args for password, not env vars
    const containerId = await this.oneboxRef.docker.createPlatformContainer({
      name: containerName,
      image: config.image,
      port: config.port,
      env: [],
      volumes: config.volumes,
      network: this.getNetworkName(),
      command: ['redis-server', '--requirepass', adminCredentials.password, '--appendonly', 'yes'],
    });

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

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

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

      logger.info(`Redis 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
      const result = await this.oneboxRef.docker.execInContainer(
        platformService.containerId,
        ['redis-cli', '-a', adminCreds.password, 'ping']
      );

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

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

    // Determine the next available DB index (1-15, reserving 0 for admin)
    const existingResources = this.oneboxRef.database.getPlatformResourcesByPlatformService(platformService.id!);
    const usedIndexes = new Set<number>();

    for (const resource of existingResources) {
      try {
        const creds = await credentialEncryption.decrypt(resource.credentialsEncrypted);
        if (creds.db) {
          usedIndexes.add(parseInt(creds.db, 10));
        }
      } catch {
        // Skip resources with corrupt credentials
      }
    }

    let dbIndex = -1;
    for (let i = 1; i <= 15; i++) {
      if (!usedIndexes.has(i)) {
        dbIndex = i;
        break;
      }
    }

    if (dbIndex === -1) {
      throw new Error('No available Redis database indexes (max 15 services per Redis instance)');
    }

    const resourceName = this.generateResourceName(userService.name);

    logger.info(`Provisioning Redis database index ${dbIndex} for service '${userService.name}'...`);

    // No server-side creation needed - Redis DB indexes exist implicitly
    // Just verify connectivity
    if (platformService.containerId) {
      const result = await this.oneboxRef.docker.execInContainer(
        platformService.containerId,
        ['redis-cli', '-a', adminCreds.password, '-n', String(dbIndex), 'ping']
      );

      if (result.exitCode !== 0 || !result.stdout.includes('PONG')) {
        throw new Error(`Failed to verify Redis database ${dbIndex}: exit code ${result.exitCode}`);
      }
    }

    logger.success(`Redis database index ${dbIndex} provisioned for service '${userService.name}'`);

    // Build the credentials and env vars
    const credentials: Record<string, string> = {
      host: containerName,
      port: '6379',
      password: adminCreds.password,
      db: String(dbIndex),
      connectionString: `redis://:${adminCreds.password}@${containerName}:6379/${dbIndex}`,
    };

    // 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: 'cache',
      name: resourceName,
      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('Redis platform service not found or not configured');
    }

    const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
    const dbIndex = credentials.db || '0';

    logger.info(`Deprovisioning Redis database index ${dbIndex} for resource '${resource.resourceName}'...`);

    // Flush the specific database
    const result = await this.oneboxRef.docker.execInContainer(
      platformService.containerId,
      ['redis-cli', '-a', adminCreds.password, '-n', dbIndex, 'FLUSHDB']
    );

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

    logger.success(`Redis database index ${dbIndex} flushed for resource '${resource.resourceName}'`);
  }
}
