import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import {
  DeleteObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { envs } from '../../config';

export interface LegajoStorageUploadInput {
  empresaId: string;
  empleadoId: string;
  tipoCodigo: string;
  documentoId: string;
  extension: string;
  mimeType: string;
  buffer: Buffer;
}

export interface LegajoStorageUploadResult {
  key: string;
  etag?: string;
  size: number;
}

/**
 * Abstracción sobre DigitalOcean Spaces (S3 compatible) para el módulo de Legajos.
 *
 * Reglas:
 * - ACL `private`: los objetos no son accesibles sin presigned URL.
 * - Convención de ruta: `legajos/{empresa_id}/{empleado_id}/{tipo_codigo}/{año}/{uuid}.{ext}`.
 * - Default de expiración configurable vía `LEGAJO_PRESIGNED_URL_EXPIRY_SEC` (default 900 s).
 */
@Injectable()
export class LegajosStorageService {
  private readonly logger = new Logger(LegajosStorageService.name);
  private readonly client: S3Client;
  private readonly bucket: string;
  private readonly defaultExpirySec: number;

  constructor() {
    this.client = new S3Client({
      endpoint: envs.doSpacesEndpoint,
      region: envs.doSpacesRegion,
      credentials: {
        accessKeyId: envs.doSpacesKey,
        secretAccessKey: envs.doSpacesSecret,
      },
    });
    this.bucket = envs.doSpacesBucket;
    this.defaultExpirySec = envs.legajoPresignedUrlExpirySec;
  }

  /**
   * Construye la ruta canónica del objeto en el storage.
   * Ej: legajos/{empresa_id}/{empleado_id}/CI/2026/{uuid}.pdf
   */
  buildStorageKey(input: {
    empresaId: string;
    empleadoId: string;
    tipoCodigo: string;
    documentoId: string;
    extension: string;
    fecha?: Date;
  }): string {
    const anio = (input.fecha ?? new Date()).getUTCFullYear();
    const ext = input.extension.replace(/^\./, '').toLowerCase();
    const tipo = input.tipoCodigo.toUpperCase();
    return `legajos/${input.empresaId}/${input.empleadoId}/${tipo}/${anio}/${input.documentoId}.${ext}`;
  }

  async upload(input: LegajoStorageUploadInput): Promise<LegajoStorageUploadResult> {
    const key = this.buildStorageKey(input);
    try {
      const result = await this.client.send(
        new PutObjectCommand({
          Bucket: this.bucket,
          Key: key,
          Body: input.buffer,
          ContentType: input.mimeType,
          ACL: 'private',
          Metadata: {
            empresa_id: input.empresaId,
            empleado_id: input.empleadoId,
            tipo_codigo: input.tipoCodigo,
          },
        }),
      );
      return { key, etag: result.ETag, size: input.buffer.length };
    } catch (err) {
      this.logger.error(`Error subiendo objeto ${key}: ${err instanceof Error ? err.message : err}`);
      throw new InternalServerErrorException('No se pudo guardar el documento en el almacenamiento.');
    }
  }

  /**
   * Genera una URL firmada de descarga con `Content-Disposition: attachment`.
   * El navegador del usuario descarga el archivo con el nombre original.
   */
  async presignedDownloadUrl(
    key: string,
    filename: string,
    expirySec?: number,
  ): Promise<string> {
    const safeName = this.encodeContentDisposition(filename);
    const command = new GetObjectCommand({
      Bucket: this.bucket,
      Key: key,
      ResponseContentDisposition: `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(filename)}`,
    });
    return getSignedUrl(this.client, command, { expiresIn: expirySec ?? this.defaultExpirySec });
  }

  /**
   * Genera una URL firmada de preview con `Content-Disposition: inline`.
   * El navegador abre el archivo (PDF / imagen) en lugar de descargarlo.
   */
  async presignedPreviewUrl(
    key: string,
    mimeType: string,
    expirySec?: number,
  ): Promise<string> {
    const command = new GetObjectCommand({
      Bucket: this.bucket,
      Key: key,
      ResponseContentDisposition: 'inline',
      ResponseContentType: mimeType,
    });
    return getSignedUrl(this.client, command, { expiresIn: expirySec ?? this.defaultExpirySec });
  }

  /**
   * Verifica que el objeto exista en el storage. No retorna el contenido.
   */
  async exists(key: string): Promise<boolean> {
    try {
      await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
      return true;
    } catch (err) {
      const code = (err as { $metadata?: { httpStatusCode?: number } })?.$metadata?.httpStatusCode;
      if (code === 404 || code === 403) return false;
      throw err;
    }
  }

  /**
   * Elimina físicamente el objeto del storage. Sólo debe invocarse desde la purga manual
   * controlada por SUPER_ADMIN. El flujo normal de "anular" NO llama a este método.
   */
  async delete(key: string): Promise<void> {
    await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
  }

  private encodeContentDisposition(filename: string): string {
    // Reemplaza caracteres no ASCII y comillas para evitar romper el header.
    return filename.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, "'");
  }
}
