import { Client as MinioClient } from 'minio';
import { StorageService, FileMetadata, UploadResult, UploadFileParams } from './StorageService';

export class MinioStorageService implements StorageService {
  private client: MinioClient;
  private bucketName: string;
  private cdnBaseUrl: string;

  constructor(
    endpoint: string,
    accessKey: string,
    secretKey: string,
    useSSL: boolean = false,
    bucketName: string = 'banatie',
    cdnBaseUrl?: string,
  ) {
    // Parse endpoint to separate hostname and port
    const cleanEndpoint = endpoint.replace(/^https?:\/\//, '');
    const [hostname, portStr] = cleanEndpoint.split(':');
    const port = portStr ? parseInt(portStr, 10) : useSSL ? 443 : 9000;

    if (!hostname) {
      throw new Error(`Invalid MinIO endpoint: ${endpoint}`);
    }

    this.client = new MinioClient({
      endPoint: hostname,
      port,
      useSSL,
      accessKey,
      secretKey,
    });
    this.bucketName = bucketName;
    // CDN base URL without bucket name (e.g., https://cdn.banatie.app)
    this.cdnBaseUrl = cdnBaseUrl || process.env['CDN_BASE_URL'] || `${useSSL ? 'https' : 'http'}://${endpoint}/${bucketName}`;
  }

  /**
   * Get file path in storage
   * Format: {orgSlug}/{projectSlug}/img/{imageId}
   */
  private getFilePath(orgSlug: string, projectSlug: string, imageId: string): string {
    return `${orgSlug}/${projectSlug}/img/${imageId}`;
  }

  /**
   * Extract file extension from original filename
   */
  private extractExtension(filename: string): string | undefined {
    if (!filename) return undefined;
    const lastDotIndex = filename.lastIndexOf('.');
    if (lastDotIndex <= 0) return undefined;
    return filename.substring(lastDotIndex + 1).toLowerCase();
  }

  /**
   * Validate storage path components
   */
  private validatePath(orgSlug: string, projectSlug: string, imageId: string): void {
    // Validate orgSlug
    if (!orgSlug || !/^[a-zA-Z0-9_-]+$/.test(orgSlug) || orgSlug.length > 50) {
      throw new Error(
        'Invalid organization slug: must be alphanumeric with dashes/underscores, max 50 chars',
      );
    }

    // Validate projectSlug
    if (!projectSlug || !/^[a-zA-Z0-9_-]+$/.test(projectSlug) || projectSlug.length > 50) {
      throw new Error(
        'Invalid project slug: must be alphanumeric with dashes/underscores, max 50 chars',
      );
    }

    // Validate imageId (UUID format)
    if (!imageId || imageId.length === 0 || imageId.length > 50) {
      throw new Error('Invalid imageId: must be 1-50 characters');
    }

    // Check for path traversal and dangerous patterns
    if (imageId.includes('..') || imageId.includes('/') || imageId.includes('\\')) {
      throw new Error('Invalid characters in imageId: path traversal not allowed');
    }

    // Prevent null bytes and control characters
    if (/[\x00-\x1f]/.test(imageId)) {
      throw new Error('Invalid imageId: control characters not allowed');
    }
  }

  async createBucket(): Promise<void> {
    const exists = await this.client.bucketExists(this.bucketName);
    if (!exists) {
      await this.client.makeBucket(this.bucketName, 'us-east-1');
      console.log(`Created bucket: ${this.bucketName}`);
    }

    // Bucket should be public for CDN access (configured via mc anonymous set download)
    console.log(`Bucket ${this.bucketName} ready for CDN access`);
  }

  async bucketExists(): Promise<boolean> {
    return await this.client.bucketExists(this.bucketName);
  }

  async uploadFile(params: UploadFileParams): Promise<UploadResult> {
    const { orgSlug, projectSlug, imageId, buffer, contentType, originalFilename } = params;

    // Validate inputs first
    this.validatePath(orgSlug, projectSlug, imageId);

    if (!buffer || buffer.length === 0) {
      throw new Error('Buffer cannot be empty');
    }

    if (!contentType || contentType.trim().length === 0) {
      throw new Error('Content type is required');
    }

    // Ensure bucket exists
    await this.createBucket();

    // Get file path: {orgSlug}/{projectSlug}/img/{imageId}
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);

    // Extract file extension from original filename
    const fileExtension = originalFilename ? this.extractExtension(originalFilename) : undefined;

    // Encode original filename to Base64 to safely store non-ASCII characters in metadata
    const originalNameEncoded = originalFilename
      ? Buffer.from(originalFilename, 'utf-8').toString('base64')
      : undefined;

    const metadata: Record<string, string> = {
      'Content-Type': contentType,
      'X-Amz-Meta-Project': projectSlug,
      'X-Amz-Meta-Organization': orgSlug,
      'X-Amz-Meta-Upload-Time': new Date().toISOString(),
    };

    if (originalNameEncoded) {
      metadata['X-Amz-Meta-Original-Name'] = originalNameEncoded;
      metadata['X-Amz-Meta-Original-Name-Encoding'] = 'base64';
    }

    if (fileExtension) {
      metadata['X-Amz-Meta-File-Extension'] = fileExtension;
    }

    console.log(`[MinIO] Uploading file to: ${this.bucketName}/${filePath}`);

    await this.client.putObject(
      this.bucketName,
      filePath,
      buffer,
      buffer.length,
      metadata,
    );

    const url = this.getPublicUrl(orgSlug, projectSlug, imageId);

    console.log(`[MinIO] CDN URL: ${url}`);

    return {
      success: true,
      filename: imageId,
      path: filePath,
      url,
      size: buffer.length,
      contentType,
      ...(originalFilename && { originalFilename }),
      ...(fileExtension && { fileExtension }),
    };
  }

  async downloadFile(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
  ): Promise<Buffer> {
    this.validatePath(orgSlug, projectSlug, imageId);
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);

    const stream = await this.client.getObject(this.bucketName, filePath);

    return new Promise((resolve, reject) => {
      const chunks: Buffer[] = [];
      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('end', () => resolve(Buffer.concat(chunks)));
      stream.on('error', reject);
    });
  }

  async streamFile(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
  ): Promise<import('stream').Readable> {
    this.validatePath(orgSlug, projectSlug, imageId);
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);

    // Return the stream directly without buffering - memory efficient!
    return await this.client.getObject(this.bucketName, filePath);
  }

  async deleteFile(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
  ): Promise<void> {
    this.validatePath(orgSlug, projectSlug, imageId);
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);
    await this.client.removeObject(this.bucketName, filePath);
  }

  /**
   * Get public CDN URL for file access
   * Returns: https://cdn.banatie.app/{orgSlug}/{projectSlug}/img/{imageId}
   */
  getPublicUrl(orgSlug: string, projectSlug: string, imageId: string): string {
    this.validatePath(orgSlug, projectSlug, imageId);
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);
    return `${this.cdnBaseUrl}/${filePath}`;
  }

  async getPresignedUploadUrl(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
    expirySeconds: number,
    contentType: string,
  ): Promise<string> {
    this.validatePath(orgSlug, projectSlug, imageId);

    if (!contentType || contentType.trim().length === 0) {
      throw new Error('Content type is required for presigned upload URL');
    }

    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);
    return await this.client.presignedPutObject(this.bucketName, filePath, expirySeconds);
  }

  async getPresignedDownloadUrl(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
    expirySeconds: number = 86400, // 24 hours default
  ): Promise<string> {
    this.validatePath(orgSlug, projectSlug, imageId);
    const filePath = this.getFilePath(orgSlug, projectSlug, imageId);
    const presignedUrl = await this.client.presignedGetObject(
      this.bucketName,
      filePath,
      expirySeconds,
    );

    // Replace internal Docker hostname with CDN URL if configured
    if (this.cdnBaseUrl) {
      // Access protected properties via type assertion for URL replacement
      const client = this.client as unknown as { host: string; port: number; protocol: string };
      const clientEndpoint = client.host + (client.port ? `:${client.port}` : '');

      return presignedUrl.replace(`${client.protocol}//${clientEndpoint}/${this.bucketName}`, this.cdnBaseUrl);
    }

    return presignedUrl;
  }

  /**
   * List files in a project's img folder
   */
  async listFiles(
    orgSlug: string,
    projectSlug: string,
    prefix?: string,
  ): Promise<FileMetadata[]> {
    this.validatePath(orgSlug, projectSlug, 'dummy');

    const basePath = `${orgSlug}/${projectSlug}/img/`;
    const searchPrefix = prefix ? `${basePath}${prefix}` : basePath;

    const files: FileMetadata[] = [];

    return new Promise((resolve, reject) => {
      const stream = this.client.listObjects(this.bucketName, searchPrefix, true);

      stream.on('data', async (obj) => {
        if (!obj.name || !obj.size) return;

        try {
          const pathParts = obj.name.split('/');
          const imageId = pathParts[pathParts.length - 1];

          if (!imageId) return;

          // Get metadata to find content type (no extension in filename)
          const metadata = await this.client.statObject(this.bucketName, obj.name);

          files.push({
            filename: imageId!,
            size: obj.size,
            contentType: metadata.metaData?.['content-type'] || 'application/octet-stream',
            lastModified: obj.lastModified || new Date(),
            etag: obj.etag || '',
            path: obj.name,
          });
        } catch (error) {
          console.error('[MinIO listFiles] Error processing file:', obj.name, error);
        }
      });

      stream.on('end', () => {
        resolve(files);
      });

      stream.on('error', (error) => {
        console.error('[MinIO listFiles] Stream error:', error);
        reject(error);
      });
    });
  }

  /**
   * Parse storage key to extract components
   * Format: {orgSlug}/{projectSlug}/img/{imageId}
   */
  parseKey(key: string): {
    orgSlug: string;
    projectSlug: string;
    imageId: string;
  } | null {
    try {
      // Match: orgSlug/projectSlug/img/imageId
      const match = key.match(/^([^/]+)\/([^/]+)\/img\/([^/]+)$/);

      if (!match) {
        return null;
      }

      const [, orgSlug, projectSlug, imageId] = match;

      if (!orgSlug || !projectSlug || !imageId) {
        return null;
      }

      return {
        orgSlug,
        projectSlug,
        imageId,
      };
    } catch {
      return null;
    }
  }

  async fileExists(
    orgSlug: string,
    projectSlug: string,
    imageId: string,
  ): Promise<boolean> {
    try {
      this.validatePath(orgSlug, projectSlug, imageId);
      const filePath = this.getFilePath(orgSlug, projectSlug, imageId);
      await this.client.statObject(this.bucketName, filePath);
      return true;
    } catch (error) {
      return false;
    }
  }
}
