/**
 * SmartStorage (S3-compatible) Platform Service Provider
 *
 * Runs the serve.zone corestore image, which embeds @push.rocks/smartstorage
 * with scoped per-bucket tenant credentials and exposes a token-guarded
 * control API for provisioning. This is the same storage component cluster
 * workers run, so onebox and the fleet share one canonical storage layer.
 */

import { BasePlatformServiceProvider } from './base.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';

interface ICorestoreObjectStorageResource {
  capability: string;
  provider: string;
  resourceName: string;
  bucketName: string;
  accessKeyId: string;
  env: Record<string, string>;
}

interface ICorestoreProvisionResponse {
  serviceId: string;
  serviceName?: string;
  resources: ICorestoreObjectStorageResource[];
  env: Record<string, string>;
}

export class SmartStorageProvider extends BasePlatformServiceProvider {
  readonly type: TPlatformServiceType = 'smartstorage';
  readonly displayName = 'S3 Storage (smartstorage)';
  readonly resourceTypes: TPlatformResourceType[] = ['bucket'];

  private static readonly CONTROL_PORT = 3000;
  private static readonly S3_PORT = 9000;

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

  async getDefaultConfig(): Promise<IPlatformServiceConfig> {
    return {
      image: 'code.foss.global/serve.zone/corestore:latest',
      port: SmartStorageProvider.S3_PORT,
      volumes: [`${await this.getPlatformDataDir('smartstorage')}:/data/corestore`],
      environment: {
        CORESTORE_DATA_DIR: '/data/corestore',
        // API token is generated on deploy 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 = await this.getDefaultConfig();
    const containerName = this.getContainerName();

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

    const platformService = await this.oneboxRef.database.getPlatformServiceByType(this.type);
    let apiToken: string;
    if (platformService?.adminCredentialsEncrypted) {
      const stored = await credentialEncryption.decrypt<{ apiToken: string }>(
        platformService.adminCredentialsEncrypted,
      );
      apiToken = stored.apiToken;
    } else {
      apiToken = credentialEncryption.generatePassword(48);
    }

    const containerId = await this.oneboxRef.docker.createPlatformContainer({
      name: containerName,
      image: config.image,
      port: SmartStorageProvider.S3_PORT,
      env: [
        'CORESTORE_DATA_DIR=/data/corestore',
        `CORESTORE_PUBLIC_HOST=${containerName}`,
        `CORESTORE_API_TOKEN=${apiToken}`,
      ],
      volumeMounts: [await this.getPlatformDataVolume('/data/corestore', 'smartstorage')],
      network: this.getNetworkName(),
      exposePorts: [SmartStorageProvider.CONTROL_PORT, SmartStorageProvider.S3_PORT],
    });

    const encryptedCreds = await credentialEncryption.encrypt({ apiToken });
    if (platformService) {
      await this.oneboxRef.database.updatePlatformService(platformService.id!, {
        containerId,
        adminCredentialsEncrypted: encryptedCreds,
        status: 'starting',
      });
    }

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

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

  async healthCheck(): Promise<boolean> {
    try {
      const response = await this.controlRequest<{ ok?: boolean }>('GET', '/health');
      return response?.ok !== false;
    } catch (error) {
      logger.info(`smartstorage health check failed: ${getErrorMessage(error)}`);
      return false;
    }
  }

  async provisionResource(userService: IService): Promise<IProvisionedResource> {
    logger.info(`Provisioning smartstorage bucket for service '${userService.name}'...`);

    const provisionResponse = await this.controlRequest<ICorestoreProvisionResponse>(
      'POST',
      '/resources/provision',
      {
        serviceId: this.getCorestoreServiceId(userService.id!),
        serviceName: userService.name,
        s3: true,
      },
    );
    const bucketResource = provisionResponse.resources.find(
      (resourceArg) => resourceArg.capability === 'objectstorage',
    );
    if (!bucketResource) {
      throw new Error('corestore did not return an objectstorage resource');
    }

    const credentials: Record<string, string> = {
      endpoint: bucketResource.env.S3_ENDPOINT,
      bucket: bucketResource.bucketName,
      accessKey: bucketResource.env.S3_ACCESS_KEY_ID,
      secretKey: bucketResource.env.S3_SECRET_ACCESS_KEY,
      region: bucketResource.env.S3_REGION || 'us-east-1',
    };

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

    logger.success(`smartstorage bucket '${bucketResource.bucketName}' provisioned with scoped credentials`);

    return {
      type: 'bucket',
      name: bucketResource.bucketName,
      credentials,
      envVars,
    };
  }

  async deprovisionResource(resource: IPlatformResource, _credentials: Record<string, string>): Promise<void> {
    logger.info(`Deprovisioning smartstorage bucket '${resource.resourceName}'...`);
    await this.controlRequest('POST', '/resources/deprovision', {
      serviceId: this.getCorestoreServiceId(resource.serviceId),
    });
    logger.success(`smartstorage bucket '${resource.resourceName}' deprovisioned`);
  }

  /**
   * Stable corestore-side service identity. Bucket names and scoped
   * credentials are derived from it, so it must never change for a service.
   */
  private getCorestoreServiceId(serviceIdArg: number): string {
    return `onebox-service-${serviceIdArg}`;
  }

  /**
   * Calls the corestore control API through the 127.0.0.1 published port.
   * The control port is never exposed beyond the host; requests carry the
   * stored API token.
   */
  private async controlRequest<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
    const platformService = await this.oneboxRef.database.getPlatformServiceByType(this.type);
    if (!platformService?.containerId) {
      throw new Error('smartstorage platform service not found or not deployed');
    }
    if (!platformService.adminCredentialsEncrypted) {
      throw new Error('smartstorage platform service has no stored API token');
    }
    const { apiToken } = await credentialEncryption.decrypt<{ apiToken: string }>(
      platformService.adminCredentialsEncrypted,
    );
    const hostPort = await this.oneboxRef.docker.getContainerHostPort(
      platformService.containerId,
      SmartStorageProvider.CONTROL_PORT,
    );
    if (!hostPort) {
      throw new Error('smartstorage control port is not published to the host');
    }
    const response = await fetch(`http://127.0.0.1:${hostPort}${path}`, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'x-corestore-token': apiToken,
      },
      ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
    });
    if (!response.ok) {
      const errorText = await response.text().catch(() => '');
      throw new Error(`corestore control API ${method} ${path} failed (${response.status}): ${errorText.slice(0, 200)}`);
    }
    return (await response.json()) as T;
  }
}
