import {
  BadRequestException,
  ForbiddenException,
  Inject,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
  DOCUMENT_CONTENT_TYPES,
  DOCUMENT_KINDS,
  DOCUMENT_MAX_BYTES,
  type DocumentDto,
  type DocumentKind,
  type DocumentsResponse,
} from '@siop/shared';
import { randomUUID } from 'node:crypto';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { FILE_STORAGE, type FileStorage } from '../files/file-storage';
import { PermissionsService } from '../permissions/permissions.service';
import { PrismaService } from '../prisma/prisma.service';

const documentInclude = {
  asset: { select: { reference: true } },
  workOrder: { select: { reference: true } },
  uploadedBy: { select: { displayName: true } },
} satisfies Prisma.DocumentInclude;

type Row = Prisma.DocumentGetPayload<{ include: typeof documentInclude }>;

export interface UploadInput {
  buffer: Buffer;
  originalName: string;
  contentType: string;
  size: number;
  kind: string;
  assetId?: string;
  workOrderId?: string;
}

@Injectable()
export class DocumentsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly permissions: PermissionsService,
    @Inject(FILE_STORAGE) private readonly storage: FileStorage,
  ) {}

  async list(filtres: {
    assetId?: string;
    workOrderId?: string;
    kind?: string;
  }): Promise<DocumentsResponse> {
    const rows = await this.prisma.document.findMany({
      where: {
        assetId: filtres.assetId || undefined,
        workOrderId: filtres.workOrderId || undefined,
        kind: (filtres.kind as DocumentKind) || undefined,
      },
      include: documentInclude,
      orderBy: { createdAt: 'desc' },
    });
    return { documents: rows.map((r) => this.toDto(r)) };
  }

  /** Upload : types fermés, 20 Mo max, rattachement REQUIS, permission
   *  d'édition sur la CIBLE (appareil → ASSETS, OT → WORK_ORDERS). */
  async upload(input: UploadInput, user: AuthenticatedUser): Promise<DocumentDto> {
    if (!(DOCUMENT_KINDS as readonly string[]).includes(input.kind)) {
      throw new BadRequestException('Type de document inconnu');
    }
    if (!(DOCUMENT_CONTENT_TYPES as readonly string[]).includes(input.contentType)) {
      throw new BadRequestException('Format refusé — PDF, JPG ou PNG uniquement');
    }
    if (input.size > DOCUMENT_MAX_BYTES) {
      throw new BadRequestException('Fichier trop lourd — 20 Mo maximum');
    }
    if (!input.assetId && !input.workOrderId) {
      throw new BadRequestException('Rattachez le document à un appareil ou à un OT');
    }
    await this.assertCible(input.assetId, input.workOrderId, user, 'edit');

    const storageKey = `documents/${randomUUID()}/${input.originalName.replace(/[^\w.\-()À-ſ ]/g, '_')}`;
    await this.storage.putObject(storageKey, input.buffer, input.contentType);
    const created = await this.prisma.document.create({
      data: {
        kind: input.kind as DocumentKind,
        fileName: input.originalName,
        storageKey,
        size: input.size,
        contentType: input.contentType,
        assetId: input.assetId,
        workOrderId: input.workOrderId,
        uploadedById: user.userId,
      },
      include: documentInclude,
    });
    return this.toDto(created);
  }

  async download(id: string): Promise<{
    stream: NodeJS.ReadableStream;
    fileName: string;
    contentType: string;
  }> {
    const doc = await this.prisma.document.findUnique({ where: { id } });
    if (!doc) throw new NotFoundException('Document inconnu');
    return {
      stream: await this.storage.getObjectStream(doc.storageKey),
      fileName: doc.fileName,
      contentType: doc.contentType,
    };
  }

  async remove(id: string, user: AuthenticatedUser): Promise<void> {
    const doc = await this.prisma.document.findUnique({ where: { id } });
    if (!doc) throw new NotFoundException('Document inconnu');
    await this.assertCible(doc.assetId ?? undefined, doc.workOrderId ?? undefined, user, 'edit');
    await this.prisma.document.delete({ where: { id } });
    await this.storage.removeObject(doc.storageKey).catch(() => {
      // le stockage peut être momentanément injoignable — la base fait foi
    });
  }

  private async assertCible(
    assetId: string | undefined,
    workOrderId: string | undefined,
    user: AuthenticatedUser,
    right: 'edit',
  ): Promise<void> {
    if (assetId) {
      const asset = await this.prisma.asset.findUnique({ where: { id: assetId } });
      if (!asset) throw new BadRequestException('Appareil inconnu');
      if (!(await this.permissions.can(user.roleId, 'ASSETS', right))) {
        throw new ForbiddenException('Droit manquant : ASSETS.edit');
      }
    }
    if (workOrderId) {
      const ot = await this.prisma.workOrder.findUnique({ where: { id: workOrderId } });
      if (!ot) throw new BadRequestException('OT inconnu');
      if (!(await this.permissions.can(user.roleId, 'WORK_ORDERS', right))) {
        throw new ForbiddenException('Droit manquant : WORK_ORDERS.edit');
      }
    }
  }

  private toDto(row: Row): DocumentDto {
    return {
      id: row.id,
      kind: row.kind,
      fileName: row.fileName,
      size: row.size,
      contentType: row.contentType,
      assetReference: row.asset?.reference ?? null,
      workOrderReference: row.workOrder?.reference ?? null,
      uploadedByName: row.uploadedBy?.displayName ?? null,
      createdAt: row.createdAt.toISOString(),
      inCorpus: row.inCorpus,
      indexedAt: row.indexedAt?.toISOString() ?? null,
      chunkCount: row.chunkCount,
    };
  }

  /** Interrupteur corpus (D3) — effectif à la prochaine réindexation. */
  async setCorpus(id: string, inCorpus: boolean): Promise<DocumentDto> {
    const doc = await this.prisma.document.findUnique({ where: { id } });
    if (!doc) throw new NotFoundException('Document inconnu');
    const updated = await this.prisma.document.update({
      where: { id },
      data: { inCorpus },
      include: documentInclude,
    });
    return this.toDto(updated);
  }
}
