import * as crypto from 'crypto';
import * as path from 'path';
import {
  S3Client,
  PutObjectCommand,
  DeleteObjectCommand,
  HeadBucketCommand,
  CreateBucketCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { LoggerService } from '@modules/shared';

export const MEDIA_STORAGE_SERVICE = Symbol('MEDIA_STORAGE_SERVICE');

export interface PresignedUploadResult {
  uploadUrl: string;
  objectKey: string;
  publicUrl: string;
}

export interface IMediaStorageService {
  upload(buffer: Buffer, originalName: string, mimeType: string, folder: string): Promise<string>;
  delete(fileUrl: string): Promise<void>;
  getPresignedUploadUrl(objectKey: string, mimeType: string, expiresInSeconds?: number): Promise<string>;
  generatePresignedUpload(
    folder: string,
    fileName: string,
    mimeType: string,
    expiresInSeconds?: number,
  ): Promise<PresignedUploadResult>;
  getPublicUrl(objectKey: string): string;
  isTrustedUrl(url: string): boolean;
}

function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
  return value;
}

@Injectable()
export class MinioMediaStorageService implements IMediaStorageService, OnModuleInit {
  private readonly s3: S3Client;
  private readonly endpoint: string;
  private readonly port: number;
  private readonly bucket: string;
  private readonly useSSL: boolean;

  constructor(private readonly logger: LoggerService) {
    const accessKey = requireEnv('MINIO_ACCESS_KEY');
    const secretKey = requireEnv('MINIO_SECRET_KEY');
    this.endpoint = process.env['MINIO_ENDPOINT'] || 'localhost';
    this.port = parseInt(process.env['MINIO_PORT'] || '9000', 10);
    this.bucket = process.env['MINIO_BUCKET'] || 'goodgo-media';
    this.useSSL = process.env['MINIO_USE_SSL'] === 'true';

    const protocol = this.useSSL ? 'https' : 'http';

    this.s3 = new S3Client({
      endpoint: `${protocol}://${this.endpoint}:${this.port}`,
      region: 'us-east-1',
      credentials: {
        accessKeyId: accessKey,
        secretAccessKey: secretKey,
      },
      forcePathStyle: true,
    });
  }

  async onModuleInit(): Promise<void> {
    try {
      await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));
      this.logger.log(`Bucket "${this.bucket}" exists`, 'MinioMediaStorageService');
    } catch (error: unknown) {
      const statusCode = (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode;
      if (statusCode === 404 || statusCode === 403) {
        try {
          this.logger.log(`Creating bucket "${this.bucket}"...`, 'MinioMediaStorageService');
          await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket }));
          this.logger.log(`Bucket "${this.bucket}" created`, 'MinioMediaStorageService');
        } catch (createError) {
          this.logger.warn(
            `Could not create bucket "${this.bucket}": ${String(createError)}. Media uploads may fail.`,
            'MinioMediaStorageService',
          );
        }
      } else {
        this.logger.warn(
          `Could not verify bucket "${this.bucket}": ${String(error)}. Media uploads may fail.`,
          'MinioMediaStorageService',
        );
      }
    }
  }

  async upload(buffer: Buffer, originalName: string, mimeType: string, folder: string): Promise<string> {
    const ext = path.extname(originalName);
    const hash = crypto.createHash('md5').update(buffer).digest('hex').slice(0, 12);
    const objectName = `${folder}/${Date.now()}-${hash}${ext}`;

    try {
      await this.s3.send(
        new PutObjectCommand({
          Bucket: this.bucket,
          Key: objectName,
          Body: buffer,
          ContentType: mimeType,
        }),
      );

      const protocol = this.useSSL ? 'https' : 'http';
      const url = `${protocol}://${this.endpoint}:${this.port}/${this.bucket}/${objectName}`;

      this.logger.log(`Media uploaded: ${objectName}`, 'MinioMediaStorageService');
      return url;
    } catch (error) {
      this.logger.error(`Media upload failed: ${objectName}`, String(error), 'MinioMediaStorageService');
      throw error;
    }
  }

  async getPresignedUploadUrl(objectKey: string, mimeType: string, expiresInSeconds = 300): Promise<string> {
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: objectKey,
      ContentType: mimeType,
    });
    return getSignedUrl(this.s3, command, { expiresIn: expiresInSeconds });
  }

  async generatePresignedUpload(
    folder: string,
    fileName: string,
    mimeType: string,
    expiresInSeconds = 300,
  ): Promise<PresignedUploadResult> {
    const ext = path.extname(fileName);
    const hash = crypto.randomBytes(8).toString('hex');
    const objectKey = `${folder}/${Date.now()}-${hash}${ext}`;

    const uploadUrl = await this.getPresignedUploadUrl(objectKey, mimeType, expiresInSeconds);
    const publicUrl = this.getPublicUrl(objectKey);

    return { uploadUrl, objectKey, publicUrl };
  }

  getPublicUrl(objectKey: string): string {
    const protocol = this.useSSL ? 'https' : 'http';
    return `${protocol}://${this.endpoint}:${this.port}/${this.bucket}/${objectKey}`;
  }

  /**
   * Validates that a URL points to our configured MinIO bucket.
   * Accepts the primary endpoint, plus an optional comma-separated list of
   * additional trusted hosts via `MINIO_TRUSTED_HOSTS` (e.g. public CDN domains).
   * Also enforces the bucket is the first path segment.
   */
  isTrustedUrl(url: string): boolean {
    if (!url || typeof url !== 'string') {
      return false;
    }
    let parsed: URL;
    try {
      parsed = new URL(url);
    } catch {
      return false;
    }

    const allowedHosts = new Set<string>();
    allowedHosts.add(this.endpoint.toLowerCase());
    allowedHosts.add(`${this.endpoint.toLowerCase()}:${this.port}`);

    const extra = process.env['MINIO_TRUSTED_HOSTS'];
    if (extra) {
      for (const h of extra.split(',')) {
        const trimmed = h.trim().toLowerCase();
        if (trimmed) allowedHosts.add(trimmed);
      }
    }

    const host = parsed.host.toLowerCase();
    if (!allowedHosts.has(host) && !allowedHosts.has(parsed.hostname.toLowerCase())) {
      return false;
    }

    // Path must start with /<bucket>/
    const expectedPrefix = `/${this.bucket}/`;
    return parsed.pathname.startsWith(expectedPrefix) && parsed.pathname.length > expectedPrefix.length;
  }

  async delete(fileUrl: string): Promise<void> {
    try {
      const urlObj = new URL(fileUrl);
      const objectKey = urlObj.pathname.replace(`/${this.bucket}/`, '');

      await this.s3.send(
        new DeleteObjectCommand({
          Bucket: this.bucket,
          Key: objectKey,
        }),
      );

      this.logger.log(`Media deleted: ${objectKey}`, 'MinioMediaStorageService');
    } catch (error) {
      this.logger.error(`Media delete failed: ${fileUrl}`, String(error), 'MinioMediaStorageService');
      throw error;
    }
  }
}
