import { Injectable, NotFoundException } from "@nestjs/common";
import { Readable } from "stream";

import { MinioService } from "../minio/minio.service";
import { FilesRepository } from "./files.repository";
import { FileRecord } from "./entities/file.entity";

export interface CreateFileInput {
  resourceId: string;
  resource: string;
  code: string;
  file: Express.Multer.File;
}

@Injectable()
export class FilesService {
  constructor(
    private readonly filesRepository: FilesRepository,
    private readonly minioService: MinioService,
  ) {}

  async upload(input: CreateFileInput): Promise<FileRecord> {
    const { resourceId, resource, code, file } = input;
    const objectName = `${resource}/${resourceId}/${Date.now()}_${file.originalname}`;
    const url = await this.minioService.uploadFile(objectName, file.buffer, file.mimetype);

    return this.filesRepository.create({
      resourceId,
      resource,
      code,
      name: file.originalname,
      url,
      size: file.size,
      mimeType: file.mimetype,
    });
  }

  /** Replace existing file row for the same resource + code (e.g. contract PDF). */
  async upsertByCode(input: CreateFileInput): Promise<FileRecord> {
    const { resourceId, resource, code } = input;
    await this.filesRepository.deleteByCode(resourceId, resource, code);
    return this.upload(input);
  }

  async uploadMany(
    resourceId: string,
    resource: string,
    files: Express.Multer.File[],
  ): Promise<FileRecord[]> {
    return Promise.all(
      files.map((file) =>
        this.upload({ resourceId, resource, code: file.fieldname, file }),
      ),
    );
  }

  /**
   * Attach already-stored files (e.g. a company profile's onboarding documents)
   * to a resource by reference — creates FileRecord rows pointing at the existing
   * object-storage URLs, without re-uploading bytes. The snapshot is fixed at call
   * time, so later changes to the source documents never alter what was attached.
   */
  async attachExistingFiles(
    resourceId: string,
    resource: string,
    files: Array<{
      code: string;
      name: string;
      url: string;
      size: number;
      mimeType?: string;
    }>,
  ): Promise<FileRecord[]> {
    return Promise.all(
      files.map((f) =>
        this.filesRepository.create({
          resourceId,
          resource,
          code: f.code,
          name: f.name,
          url: f.url,
          size: f.size,
          mimeType: f.mimeType ?? "application/octet-stream",
        }),
      ),
    );
  }

  async findById(id: string): Promise<FileRecord> {
    const record = await this.filesRepository.findById(id);
    if (!record) throw new NotFoundException(`File ${id} not found`);
    return record;
  }

  findByResource(resourceId: string, resource: string): Promise<FileRecord[]> {
    return this.filesRepository.findByResource(resourceId, resource);
  }

  async findByCode(
    resourceId: string,
    resource: string,
    code: string,
  ): Promise<FileRecord> {
    const record = await this.filesRepository.findByCode(resourceId, resource, code);
    if (!record)
      throw new NotFoundException(
        `File with code "${code}" not found for ${resource} ${resourceId}`,
      );
    return record;
  }

  async streamById(id: string): Promise<{ stream: Readable; record: FileRecord }> {
    const record = await this.findById(id);
    const objectName = this.minioService.getObjectNameFromUrl(record.url);
    const stream = await this.minioService.getFileStream(objectName);
    return { stream, record };
  }
}
