import { getLocaleText } from '@hed-hog/api-locale';
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
import { PrismaService } from '@hed-hog/api-prisma';
import {
    BadRequestException,
    forwardRef,
    Inject,
    Injectable,
    NotFoundException,
    OnModuleInit
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { createWriteStream, promises as fs } from 'fs';
import { basename, dirname } from 'path';
import { pipeline } from 'stream/promises';
import { SettingService } from '../setting/setting.service';
import { DeleteDTO } from './dto/delete.dto';
import { AbstractProvider } from './provider/abstract.provider';
import { EnumProvider } from './provider/provider.enum';
import { ProviderFactory } from './provider/provider.factory';

@Injectable()
export class FileService implements OnModuleInit {
  private providerId: number;
  private mimetypes: Record<string, number> = {};

  private hasMojibake(value: string): boolean {
    return /Ã.|Â.|â[\u0080-\u00BF]/.test(value);
  }

  private normalizeUploadedFilename(filename: string): string {
    if (!filename) {
      return filename;
    }

    if (!this.hasMojibake(filename)) {
      return filename;
    }

    const decoded = Buffer.from(filename, 'latin1').toString('utf8');
    return this.hasMojibake(decoded) ? filename : decoded;
  }

  constructor(
    @Inject(forwardRef(() => PrismaService))
    private readonly prismaService: PrismaService,
    @Inject(forwardRef(() => PaginationService))
    private readonly paginationService: PaginationService,
    @Inject(forwardRef(() => SettingService))
    private readonly settingService: SettingService,
    @Inject(forwardRef(() => JwtService))
    private readonly jwtService: JwtService,
  ) { }

  async onModuleInit() {
    await this.getProvider();
  }

  async getProvider(): Promise<AbstractProvider> {
    const settings = await this.settingService.getSettingValues([
      'storage',
      'storage-local-path',
      'storage-s3-key',
      'storage-s3-secret',
      'storage-s3-region',
      'storage-s3-bucket',
      'storage-max-size',
      'storage-accept-mimetype',
      'storage-abs-account',
      'storage-abs-key',
      'storage-abs-container',
      'storage-gcs-keyfile',
    ]);

    if (!settings['storage']) {
      throw new BadRequestException(
        `You must set the storage provider in the setting.`,
      );
    }

    const providerName = settings['storage'];
    const provider = ProviderFactory.create(
      providerName as EnumProvider,
      settings,
      this.jwtService,
    );

    const providerData = await this.prismaService.file_provider.findFirst({
      where: {
        slug: providerName,
      },
      select: {
        id: true,
      },
    });

    if (!providerData) {
      throw new BadRequestException(`Provider ${providerName} not found.`);
    }

    this.providerId = providerData.id;
    return provider;
  }

  async getMimeType(mimetype: string) {

    if (!mimetype) {
      throw new BadRequestException(`No mimetype provided`);
    }

    if (this.mimetypes[mimetype]) {
      return this.mimetypes[mimetype];
    }

    let result = await this.prismaService.file_mimetype.findFirst({
      where: {
        name: mimetype,
      },
    });

    if (!result) {
      result = await this.prismaService.file_mimetype.create({
        data: {
          name: mimetype,
        },
      });
    }

    return (this.mimetypes[mimetype] = result.id);
  }

  async acceptMimetypes(mimetype: string) {

    if (!mimetype) {
      throw new BadRequestException(`No mimetype provided`);
    }

    await this.getProvider();

    const settings = await this.settingService.getSettingValues([
      'storage',
      'storage-local-path',
      'storage-s3-key',
      'storage-s3-secret',
      'storage-s3-region',
      'storage-s3-bucket',
      'storage-max-size',
      'storage-accept-mimetype',
      'storage-abs-account',
      'storage-abs-key',
      'storage-abs-container',
      'storage-gcs-keyfile',
    ]);

    const acceptMimetypes = settings['storage-accept-mimetype'];
    return acceptMimetypes.indexOf(mimetype) !== -1;
  }

  async maxFileSize(size: number) {

    if (!size) {
      throw new BadRequestException(`No size provided`);
    }

      await this.getProvider();
    
      const settings = await this.settingService.getSettingValues([
      'storage',
      'storage-local-path',
      'storage-s3-key',
      'storage-s3-secret',
      'storage-s3-region',
      'storage-s3-bucket',
      'storage-max-size',
      'storage-accept-mimetype',
      'storage-abs-account',
      'storage-abs-key',
      'storage-abs-container',
      'storage-gcs-keyfile',
    ]);

    const maxSize = settings['storage-max-size'];

    return size <= Number(maxSize);
  }

  async uploadFromString(
    destination: string,
    filename: string,
    filecontent: string,
    mimetype = 'text/plain',
  ) {

    if (!filecontent) {
      throw new BadRequestException(`No file content provided`);
    }

    if (!destination) {
      throw new BadRequestException(`No destination provided`);
    }

    if (!filename) {
      throw new BadRequestException(`No filename provided`);
    }

    const provider = await this.getProvider();

    if (!(await this.acceptMimetypes(mimetype))) {
      throw new BadRequestException(`Invalid file type: ${mimetype}`);
    }

    const url = await provider.uploadFromString(
      destination,
      filename,
      filecontent,
      mimetype,
    );

    const file = await this.prismaService.file.create({
      data: {
        filename,
        path: url,
        provider_id: this.providerId,
        location: destination,
        mimetype_id: await this.getMimeType(mimetype),
        size: filecontent.length,
      },
    });

    return file;
  }

  async getStorage() {
    const settings = await this.settingService.getSettingValues([
      'storage',
    ]);

    return settings['storage']; 
  }

  private getProviderPath(storage: string, filepath: string) {
    if (storage === EnumProvider.LOCAL) {
      return filepath;
    }

    try {
      return new URL(filepath).pathname;
    } catch {
      return filepath;
    }
  }

  private mapStorageNotFoundError(error: any, fileId: number) {
    const missingInStorage =
      error?.name === 'NoSuchKey' ||
      error?.Code === 'NoSuchKey' ||
      error?.$metadata?.httpStatusCode === 404;

    if (missingInStorage) {
      throw new NotFoundException(`File not found in storage: ${fileId}`);
    }

    throw error;
  }

  async openReadStreamById(fileId: number) {
    if (!fileId) {
      throw new BadRequestException(`No file ID provided`);
    }

    const file = await this.prismaService.file.findUnique({
      where: {
        id: fileId,
      },
      include: {
        file_mimetype: true,
      },
    });

    if (!file) {
      throw new NotFoundException(`File not found: ${fileId}`);
    }

    const storage = await this.getStorage();
    const provider = await this.getProvider();
    const providerPath = this.getProviderPath(storage, file.path);

    let metadata: any;
    try {
      metadata = await provider.metaData(providerPath);
    } catch {
      metadata = undefined;
    }

    try {
      const stream = await provider.readStream(providerPath);
      return {
        file,
        stream,
        size: file.size ?? this.getFileSizeFromMetadata(metadata),
      };
    } catch (error) {
      this.mapStorageNotFoundError(error, fileId);
    }
  }

  async downloadToPath(
    fileId: number,
    destinationPath: string,
    options?: { maxBytes?: number },
  ) {
    if (!destinationPath) {
      throw new BadRequestException(`No destination path provided`);
    }

    const { file, stream, size } = await this.openReadStreamById(fileId);

    if (options?.maxBytes && size && size > options.maxBytes) {
      throw new BadRequestException(
        `File too large for streaming download: ${size} bytes (max ${options.maxBytes})`,
      );
    }

    await fs.mkdir(dirname(destinationPath), { recursive: true });

    try {
      await pipeline(stream, createWriteStream(destinationPath));
      const stats = await fs.stat(destinationPath);

      if (options?.maxBytes && stats.size > options.maxBytes) {
        await fs.unlink(destinationPath).catch(() => undefined);
        throw new BadRequestException(
          `File too large for streaming download: ${stats.size} bytes (max ${options.maxBytes})`,
        );
      }

      return {
        file,
        path: destinationPath,
        size: stats.size,
      };
    } catch (error) {
      await fs.unlink(destinationPath).catch(() => undefined);
      throw error;
    }
  }

  async getBuffer(fileId: number) {

    if (!fileId) {
      throw new BadRequestException(`No file ID provided`);
    }

    const file = await this.prismaService.file.findUnique({
      where: {
        id: fileId,
      },
      include: {
        file_mimetype: true,
      },
    });

    if (!file) {
      throw new NotFoundException(`File not found: ${fileId}`);
    }
    
    const storage = await this.getStorage();

    const provider = await this.getProvider();

    const providerPath = this.getProviderPath(storage, file.path);

    let providerBuffer: Buffer;
    try {
      providerBuffer = await provider.buffer(providerPath);
    } catch (error: any) {
      const missingInStorage =
        error?.name === 'NoSuchKey' ||
        error?.Code === 'NoSuchKey' ||
        error?.$metadata?.httpStatusCode === 404;

      if (missingInStorage) {
        throw new NotFoundException(`File not found in storage: ${fileId}`);
      }

      throw error;
    }

    const buffer = Buffer.from(providerBuffer);

    return {
      file,
      buffer,
    };
  }

  async getLocalFile(fileId: number, res: any){
    const { buffer, file } = await this.getBuffer(fileId);
    res.set({
      'Content-Type': file.file_mimetype.name,
      'Content-Length': buffer.length,
    });
    res.send(buffer);
  }

  async upload(destination: string, fileBuffer: MulterFile) {

    if (!fileBuffer) {
      throw new BadRequestException(`No file provided`);
    }

    if (!destination) {
      throw new BadRequestException(`No destination provided`);
    }

    const provider = await this.getProvider();

    if (!(await this.acceptMimetypes(fileBuffer.mimetype))) {
      throw new BadRequestException(
        `Invalid file type: ${fileBuffer.mimetype}`,
      );
    }

    if (!(await this.maxFileSize(fileBuffer.size))) {
      throw new BadRequestException(`File too large: ${fileBuffer.size} bytes`);
    }

    const normalizedFilename = this.normalizeUploadedFilename(
      fileBuffer.originalname,
    );

    const normalizedFile = {
      ...fileBuffer,
      originalname: normalizedFilename,
    };

    const url = await provider.upload(destination, normalizedFile);

    const file = await this.prismaService.file.create({
      data: {
        filename: normalizedFilename,
        path: url,
        provider_id: this.providerId,
        location: destination,
        mimetype_id: await this.getMimeType(fileBuffer.mimetype),
        size: fileBuffer.size,
      },
    });

    return file;
  }

  async uploadFromPath(
    destination: string,
    filePath: string,
    metadata?: {
      originalname?: string;
      filename?: string;
      mimetype?: string;
      size?: number;
      encoding?: string;
    },
  ) {
    if (!filePath) {
      throw new BadRequestException(`No file path provided`);
    }

    if (!destination) {
      throw new BadRequestException(`No destination provided`);
    }

    const stats = await fs.stat(filePath).catch(() => null);
    if (!stats || !stats.isFile()) {
      throw new NotFoundException(`File path not found: ${filePath}`);
    }

    const size = metadata?.size ?? stats.size;
    const mimetype = metadata?.mimetype ?? 'application/octet-stream';
    const originalname = this.normalizeUploadedFilename(
      metadata?.originalname ?? metadata?.filename ?? basename(filePath),
    );

    if (!(await this.acceptMimetypes(mimetype))) {
      throw new BadRequestException(`Invalid file type: ${mimetype}`);
    }

    if (!(await this.maxFileSize(size))) {
      throw new BadRequestException(`File too large: ${size} bytes`);
    }

    const provider = await this.getProvider();
    const providerWithPathUpload = provider as AbstractProvider & {
      uploadFromPath?: (
        destination: string,
        filePath: string,
        options?: { filename?: string; mimetype?: string },
      ) => Promise<string>;
    };

    let url: string;
    if (typeof providerWithPathUpload.uploadFromPath === 'function') {
      url = await providerWithPathUpload.uploadFromPath(destination, filePath, {
        filename: originalname,
        mimetype,
      });
    } else {
      // Fallback explícito para providers legados sem suporte nativo por path.
      const buffer = await fs.readFile(filePath);
      url = await provider.upload(destination, {
        fieldname: 'file',
        originalname,
        encoding: metadata?.encoding ?? '7bit',
        mimetype,
        size,
        destination: '',
        filename: originalname,
        path: filePath,
        buffer,
      });
    }

    const file = await this.prismaService.file.create({
      data: {
        filename: originalname,
        path: url,
        provider_id: this.providerId,
        location: destination,
        mimetype_id: await this.getMimeType(mimetype),
        size,
      },
    });

    return file;
  }

  async delete(locale:string,{ ids }: DeleteDTO) {
    const files = await this.prismaService.file.findMany({
      where: {
        id: {
          in: ids,
        },
      },
      select: {
        id: true,
        path: true,
      },
    });

    if (files.length !== ids.length) {
      throw new BadRequestException(getLocaleText('file.delete.some_not_found', locale, `Some files not found`));
    }

    for (const file of files) {
      await (await this.getProvider()).delete(file.path);

      await this.prismaService.file.deleteMany({
        where: {
          id: file.id,
        },
      });
    }

    return files.map((file) => file.id);
  }

  async getFiles(paginationParams: PaginationDTO) {
    const fields = ['filename', 'path'];
    const OR = this.prismaService.createInsensitiveSearch(
      fields,
      paginationParams,
    );

    return this.paginationService.paginate(
      this.prismaService.file,
      paginationParams,
      {
        where: {
          OR,
        },
      },
    );
  }

  async get(fileId: number) {
    const file = await this.prismaService.file.findUnique({
      where: { id: fileId },
    });

    if (!file) {
      throw new NotFoundException(`File not found: ${fileId}`);
    }

    return file;
  }

  async readStream(filepath: string) {
    return (await this.getProvider()).readStream(filepath);
  }

  private getFileSizeFromMetadata(metadata: any): number | undefined {
    if (!metadata) {
      return undefined;
    }

    if (typeof metadata.size === 'number') {
      return metadata.size;
    }

    if (typeof metadata.ContentLength === 'number') {
      return metadata.ContentLength;
    }

    if (typeof metadata.contentLength === 'number') {
      return metadata.contentLength;
    }

    if (
      Array.isArray(metadata) &&
      metadata[0] &&
      metadata[0].size !== undefined
    ) {
      const gcsSize = Number(metadata[0].size);
      return Number.isNaN(gcsSize) ? undefined : gcsSize;
    }

    return undefined;
  }

  async download(locale:string, token: string) {
    try {
      const { filepath } = this.jwtService.verify(token, {
        secret: process.env.JWT_SECRET,
      });

      // Buscar o arquivo no banco para pegar nome original e mimetype
      const file = await this.prismaService.file.findFirst({
        where: { path: filepath },
        include: {
          file_mimetype: true,
        },
      });

      if (!file) {
        throw new NotFoundException(getLocaleText('file.download.not_found', locale, `Not found: ${filepath}`));
      }

      const provider = await this.getProvider();
      const metadata = await provider.metaData(filepath);
      const stream = await provider.readStream(filepath);
      const size = this.getFileSizeFromMetadata(metadata);
      
      return {
        stream,
        filename: file?.filename || 'download',
        mimetype: file?.file_mimetype?.name || 'application/octet-stream',
        size,
      };
    } catch (error: any) {
      throw new BadRequestException(getLocaleText('file.download.invalid_token', locale, error.message));
    }
  }

  async openById(locale: string, fileId: number) {
    const file = await this.prismaService.file.findFirst({
      where: { id: fileId },
      include: {
        file_mimetype: true,
      },
    });

    if (!file) {
      throw new NotFoundException(
        getLocaleText('file.download.not_found', locale, `Not found: ${fileId}`),
      );
    }

    const storage = await this.getStorage();
    const provider = await this.getProvider();
    const providerPath = this.getProviderPath(storage, file.path);
    const metadata = await provider.metaData(providerPath);
    const stream = await provider.readStream(providerPath);
    const size = this.getFileSizeFromMetadata(metadata);

    return {
      stream,
      filename: file?.filename || 'download',
      mimetype: file?.file_mimetype?.name || 'application/octet-stream',
      size,
    };
  }

  async tempURL(filepath: string, expires = 3600) {
    return (await this.getProvider()).tempURL(filepath, expires);
  }

  async tempOpenURL(filepath: string, expires = 3600) {
    const url = await this.tempURL(filepath, expires);

    if (typeof url === 'string') {
      return url.replace('/file/download/', '/file/open/');
    }

    return url;
  }

  createDownloadToken(filepath: string, expires = 3600) {
    if (!filepath) {
      throw new BadRequestException(`No file path provided`);
    }

    return this.jwtService.sign(
      { filepath },
      {
        secret: process.env.JWT_SECRET,
        expiresIn: `${Math.max(1, Number(expires) || 3600)}s`,
      },
    );
  }
}
