/**
 * ClickHouse Platform Service Provider
 */

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

export class ClickHouseProvider extends BasePlatformServiceProvider {
  readonly type: TPlatformServiceType = 'clickhouse';
  readonly displayName = 'ClickHouse';
  readonly resourceTypes: TPlatformResourceType[] = ['database'];

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

  async getDefaultConfig(): Promise<IPlatformServiceConfig> {
    return {
      image: 'clickhouse/clickhouse-server:latest',
      port: 8123, // HTTP interface
      volumes: [`${await this.getPlatformDataDir('clickhouse')}:/var/lib/clickhouse`],
      environment: {
        CLICKHOUSE_DB: 'default',
        // Password will be generated and stored encrypted
      },
    };
  }

  getEnvVarMappings(): IEnvVarMapping[] {
    return [
      { envVar: 'CLICKHOUSE_HOST', credentialPath: 'host' },
      { envVar: 'CLICKHOUSE_PORT', credentialPath: 'port' },
      { envVar: 'CLICKHOUSE_HTTP_PORT', credentialPath: 'httpPort' },
      { envVar: 'CLICKHOUSE_DATABASE', credentialPath: 'database' },
      { envVar: 'CLICKHOUSE_USER', credentialPath: 'username' },
      { envVar: 'CLICKHOUSE_PASSWORD', credentialPath: 'password' },
      { envVar: 'CLICKHOUSE_URL', credentialPath: 'connectionUrl' },
    ];
  }

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

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

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

    // Check if data directory has existing ClickHouse data
    // ClickHouse creates 'metadata' directory on first startup
    try {
      const stat = await plugins.fs.stat(`${dataDir}/metadata`);
      dataExists = stat.isDirectory;
      logger.info(`ClickHouse data directory exists with metadata folder`);
    } catch {
      // metadata directory doesn't exist, this is a fresh install
      dataExists = false;
    }

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

    // Ensure data directory exists
    try {
      await plugins.fs.mkdir(dataDir, { recursive: true });
    } catch (e) {
      // Directory might already exist
      if (!plugins.isAlreadyExistsError(e)) {
        logger.warn(`Could not create ClickHouse data directory: ${getErrorMessage(e)}`);
      }
    }

    // Create container using Docker API
    // ClickHouse uses environment variables for initial setup
    const envVars = [
      `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1`,
      `CLICKHOUSE_USER=${adminCredentials.username}`,
      `CLICKHOUSE_PASSWORD=${adminCredentials.password}`,
    ];

    const containerId = await this.oneboxRef.docker.createPlatformContainer({
      name: containerName,
      image: config.image,
      port: config.port,
      env: envVars,
      volumes: config.volumes,
      volumeMounts: [await this.getPlatformDataVolume('/var/lib/clickhouse', 'clickhouse')],
      network: this.getNetworkName(),
      exposePorts: [8123, 9000], // HTTP and native TCP ports
    });

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

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

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

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

      logger.info(`ClickHouse 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
      // Note: ClickHouse image has wget but not curl - use full path for reliability
      const result = await this.oneboxRef.docker.execInContainer(
        platformService.containerId,
        ['/usr/bin/wget', '-q', '-O', '-', 'http://localhost:8123/ping']
      );

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

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

    const adminCreds = await credentialEncryption.decrypt<{ username: string; password: string }>(
      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 ClickHouse database '${dbName}' for service '${userService.name}'...`);

    // Use docker exec to provision inside the container (avoids host port mapping issues)
    const queries = [
      `CREATE DATABASE IF NOT EXISTS ${dbName}`,
      `CREATE USER IF NOT EXISTS ${username} IDENTIFIED BY '${password}'`,
      `GRANT ALL ON ${dbName}.* TO ${username}`,
    ];

    for (const query of queries) {
      await this.execClickHouseQuery(platformService.containerId, adminCreds, query);
    }

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

    // Build the credentials and env vars
    const credentials: Record<string, string> = {
      host: containerName,
      port: '9000', // Native TCP port
      httpPort: '8123',
      database: dbName,
      username,
      password,
      connectionUrl: `http://${username}:${password}@${containerName}:8123/?database=${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 = await this.oneboxRef.database.getPlatformServiceByType(this.type);
    if (!platformService || !platformService.adminCredentialsEncrypted || !platformService.containerId) {
      throw new Error('ClickHouse platform service not found or not configured');
    }

    const adminCreds = await credentialEncryption.decrypt<{ username: string; password: string }>(
      platformService.adminCredentialsEncrypted,
    );

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

    try {
      await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP USER IF EXISTS ${credentials.username}`);
      await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP DATABASE IF EXISTS ${resource.resourceName}`);
      logger.success(`ClickHouse database '${resource.resourceName}' dropped`);
    } catch (e) {
      logger.error(`Failed to deprovision ClickHouse database: ${getErrorMessage(e)}`);
      throw e;
    }
  }

  /**
   * Execute a ClickHouse SQL query via docker exec inside the container
   */
  private async execClickHouseQuery(
    containerId: string,
    adminCreds: { username: string; password: string },
    query: string
  ): Promise<string> {
    const result = await this.oneboxRef.docker.execInContainer(
      containerId,
      [
        'clickhouse-client',
        '--user', adminCreds.username,
        '--password', adminCreds.password,
        '--query', query,
      ]
    );

    if (result.exitCode !== 0) {
      throw new Error(`ClickHouse query failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
    }

    return result.stdout;
  }
}
