import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type {
  RequestApprove,
  RequestCreate,
  RequestReject,
  RequestsResponse,
  RequestSummary,
  WorkOrderDetail,
} from '@siop/shared';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { PermissionsService } from '../permissions/permissions.service';
import { PrismaService } from '../prisma/prisma.service';
import { WorkOrdersService } from '../work-orders/work-orders.service';

const requestInclude = {
  asset: { include: { location: { include: { parent: true } } } },
  requestedBy: true,
  workOrder: true,
} satisfies Prisma.RequestInclude;

type RequestRow = Prisma.RequestGetPayload<{ include: typeof requestInclude }>;

@Injectable()
export class RequestsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly permissions: PermissionsService,
    private readonly workOrders: WorkOrdersService,
  ) {}

  private async scope(user: AuthenticatedUser): Promise<Prisma.RequestWhereInput> {
    const viewOther = await this.permissions.can(user.roleId, 'REQUESTS', 'viewOther');
    return viewOther ? {} : { requestedById: user.userId };
  }

  async list(user: AuthenticatedUser): Promise<RequestsResponse> {
    const rows = await this.prisma.request.findMany({
      where: await this.scope(user),
      include: requestInclude,
      orderBy: { createdAt: 'desc' },
    });
    rows.sort((a, b) => Number(b.isPersonTrapped) - Number(a.isPersonTrapped));
    return { requests: rows.map((r) => this.toDto(r)) };
  }

  async create(dto: RequestCreate, user: AuthenticatedUser): Promise<RequestSummary> {
    const asset = await this.prisma.asset.findUnique({ where: { id: dto.assetId } });
    if (!asset) throw new BadRequestException('Équipement inconnu');
    for (let essai = 0; ; essai++) {
      try {
        const created = await this.prisma.request.create({
          data: {
            reference: await this.nextReference(),
            description: dto.description,
            isPersonTrapped: dto.isPersonTrapped ?? false,
            assetId: dto.assetId,
            requestedById: user.userId,
          },
          include: requestInclude,
        });
        return this.toDto(created);
      } catch (e) {
        if (
          e instanceof Prisma.PrismaClientKnownRequestError &&
          e.code === 'P2002' &&
          essai < 3
        ) {
          continue;
        }
        throw e;
      }
    }
  }

  /** Approuver = créer l'OT lié (1-1). Une demande ne se traite qu'une fois. */
  async approve(
    id: string,
    dto: RequestApprove,
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    const request = await this.prisma.request.findUnique({
      where: { id },
      include: requestInclude,
    });
    if (!request) throw new NotFoundException('Demande inconnue');
    if (request.status !== 'RECEIVED') {
      throw new ConflictException('Cette demande a déjà été traitée');
    }
    const created = await this.workOrders.createRaw({
      title: dto.title ?? request.description.slice(0, 120),
      description: `${request.description}\n\n(Demande ${request.reference} — ${this.requesterLabel(request)})`,
      type: 'CORRECTIVE',
      priority: dto.priority ?? (request.isPersonTrapped ? 'PERSON_TRAPPED' : 'MEDIUM'),
      assetId: request.assetId,
      dueDate: dto.dueDate,
      assigneeIds: dto.assigneeIds,
      createdById: user.userId,
      eventKind: 'FROM_REQUEST',
      eventMessage: `OT créé depuis la demande ${request.reference}`,
    });
    await this.prisma.request.update({
      where: { id },
      data: { status: 'APPROVED', workOrderId: created.id },
    });
    return this.workOrders.get(created.id, user);
  }

  async reject(
    id: string,
    dto: RequestReject,
    user: AuthenticatedUser,
  ): Promise<RequestSummary> {
    void user;
    const request = await this.prisma.request.findUnique({ where: { id } });
    if (!request) throw new NotFoundException('Demande inconnue');
    if (request.status !== 'RECEIVED') {
      throw new ConflictException('Cette demande a déjà été traitée');
    }
    const updated = await this.prisma.request.update({
      where: { id },
      data: { status: 'REJECTED', rejectionReason: dto.reason },
      include: requestInclude,
    });
    return this.toDto(updated);
  }

  private async nextReference(): Promise<string> {
    const [{ nextval }] = await this.prisma.$queryRaw<[{ nextval: bigint }]>`
      SELECT nextval('request_ref_seq')`;
    return `DEM-${new Date().getFullYear()}-${String(nextval).padStart(4, '0')}`;
  }

  private requesterLabel(row: RequestRow): string {
    return row.requestedBy?.displayName ?? row.requesterName ?? 'Portail';
  }

  private toDto(row: RequestRow): RequestSummary {
    return {
      id: row.id,
      reference: row.reference,
      description: row.description,
      isPersonTrapped: row.isPersonTrapped,
      status: row.status,
      rejectionReason: row.rejectionReason,
      assetId: row.assetId,
      assetReference: row.asset.reference,
      siteName: row.asset.location.parent?.name ?? row.asset.location.name,
      requesterLabel: this.requesterLabel(row),
      workOrder: row.workOrder
        ? {
            id: row.workOrder.id,
            reference: row.workOrder.reference,
            status: row.workOrder.status,
          }
        : null,
      createdAt: row.createdAt.toISOString(),
    };
  }
}
