import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
  BILAN_FIELD_LABELS,
  REQUIRED_BILAN_FIELDS,
  WORK_ORDER_STATUS_LABELS,
  WORK_ORDER_TRANSITIONS,
  type AssigneesUpdate,
  type BilanField,
  type ChecklistItemDto,
  type ChecklistPatch,
  type ReportUpsert,
  type TransitionRequest,
  type WorkOrderCreate,
  type WorkOrderDetail,
  type WorkOrderStatus,
  type WorkOrdersResponse,
  type WorkOrderSummary,
} from '@siop/shared';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { PartsService } from '../parts/parts.service';
import { PermissionsService } from '../permissions/permissions.service';
import { PrismaService } from '../prisma/prisma.service';

const detailInclude = {
  asset: { include: { location: { include: { parent: true } } } },
  assignees: true,
  createdBy: true,
  request: { include: { requestedBy: true } },
  events: { include: { by: true }, orderBy: { createdAt: 'desc' as const } },
  checklist: { include: { doneBy: true }, orderBy: { label: 'asc' as const } },
  // R3 — coûts figés
  stockMovements: {
    where: { kind: 'CONSUMPTION' as const },
    include: { part: true },
    orderBy: { createdAt: 'asc' as const },
  },
  laborTimes: { include: { user: true }, orderBy: { createdAt: 'asc' as const } },
  report: {
    include: {
      doorState: true,
      cabinPosition: true,
      anomaly: true,
      externalCause: true,
      actionTaken: true,
      componentConcerned: true,
    },
  },
} satisfies Prisma.WorkOrderInclude;

type DetailRow = Prisma.WorkOrderGetPayload<{ include: typeof detailInclude }>;

const initialsOf = (name: string) =>
  name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]!.toUpperCase()).join('');

const toPersonne = (u: { id: string; displayName: string }) => ({
  id: u.id,
  displayName: u.displayName,
  initials: initialsOf(u.displayName),
});

/** Champ du bilan → colonne du rapport (validation des référentiels). */
const REPORT_FIELDS: Record<string, BilanField> = {
  doorStateId: 'DOOR_STATE',
  cabinPositionId: 'CABIN_POSITION',
  anomalyId: 'ANOMALY',
  externalCauseId: 'EXTERNAL_CAUSE',
  actionTakenId: 'ACTION_TAKEN',
  componentConcernedId: 'COMPONENT_CONCERNED',
};

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

  /** Invariant « voir autre » : sans le droit, on ne voit que SES OT. */
  private async scope(user: AuthenticatedUser): Promise<Prisma.WorkOrderWhereInput> {
    const viewOther = await this.permissions.can(user.roleId, 'WORK_ORDERS', 'viewOther');
    if (viewOther) return {};
    return {
      OR: [
        { assignees: { some: { id: user.userId } } },
        { createdById: user.userId },
      ],
    };
  }

  async list(user: AuthenticatedUser): Promise<WorkOrdersResponse> {
    const rows = await this.prisma.workOrder.findMany({
      where: await this.scope(user),
      include: {
        asset: { include: { location: { include: { parent: true } } } },
        assignees: true,
      },
      // Dernière activité d'abord : un OT qui vient d'être clôturé remonte
      // (« Interventions récentes » du tableau de bord = tête de liste).
      orderBy: { updatedAt: 'desc' },
    });
    // « personne bloquée » ACTIVE saute en tête, toujours (tri stable)
    const urgenceActive = (r: (typeof rows)[number]) =>
      r.priority === 'PERSON_TRAPPED' && r.status !== 'DONE' && r.status !== 'CANCELLED';
    rows.sort((a, b) => Number(urgenceActive(b)) - Number(urgenceActive(a)));
    return { workOrders: rows.map((r) => this.toSummary(r)) };
  }

  async get(id: string, user: AuthenticatedUser): Promise<WorkOrderDetail> {
    const row = await this.prisma.workOrder.findFirst({
      where: { AND: [{ id }, await this.scope(user)] },
      include: detailInclude,
    });
    if (!row) throw new NotFoundException('OT inconnu');
    return this.toDetail(row);
  }

  async create(dto: WorkOrderCreate, user: AuthenticatedUser): Promise<WorkOrderDetail> {
    const asset = await this.prisma.asset.findUnique({ where: { id: dto.assetId } });
    if (!asset) throw new BadRequestException('Équipement inconnu');
    const created = await this.createRaw({
      ...dto,
      createdById: user.userId,
      eventKind: 'CREATED',
      eventMessage: 'OT créé',
    });
    return this.get(created.id, user);
  }

  /** Création partagée (OT manuel, approbation de demande, préventif R2.2). */
  async createRaw(input: {
    title: string;
    description?: string;
    type: WorkOrderCreate['type'];
    priority?: WorkOrderCreate['priority'];
    assetId: string;
    dueDate?: string;
    assigneeIds?: string[];
    createdById?: string;
    eventKind: string;
    eventMessage: string;
  }): Promise<{ id: string }> {
    for (let essai = 0; ; essai++) {
      try {
        return await this.prisma.workOrder.create({
          data: {
            reference: await this.nextReference(),
            title: input.title,
            description: input.description,
            type: input.type,
            priority: input.priority ?? 'NONE',
            assetId: input.assetId,
            dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
            createdById: input.createdById,
            assignees: input.assigneeIds?.length
              ? { connect: input.assigneeIds.map((id) => ({ id })) }
              : undefined,
            events: {
              create: {
                kind: input.eventKind,
                message: input.eventMessage,
                byId: input.createdById,
              },
            },
          },
          select: { id: true },
        });
      } catch (e) {
        // collision de référence (concurrence) : on retente
        if (
          e instanceof Prisma.PrismaClientKnownRequestError &&
          e.code === 'P2002' &&
          essai < 3
        ) {
          continue;
        }
        throw e;
      }
    }
  }

  private async nextReference(): Promise<string> {
    // Séquence Postgres : insensible à la concurrence (jamais de collision)
    const [{ nextval }] = await this.prisma.$queryRaw<[{ nextval: bigint }]>`
      SELECT nextval('work_order_ref_seq')`;
    return `OT-${new Date().getFullYear()}-${String(nextval).padStart(4, '0')}`;
  }

  /** Machine à états stricte + garde de clôture. */
  async transition(
    id: string,
    dto: TransitionRequest,
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    const detail = await this.get(id, user);
    await this.assertVersion(id, dto.baseUpdatedAt);
    if (!WORK_ORDER_TRANSITIONS[detail.status].includes(dto.to)) {
      throw new ConflictException(
        `Transition interdite : ${WORK_ORDER_STATUS_LABELS[detail.status]} → ${WORK_ORDER_STATUS_LABELS[dto.to]}`,
      );
    }
    if (dto.to === 'DONE' && detail.closureBlockers.length > 0) {
      throw new ConflictException(
        `Clôture bloquée : ${detail.closureBlockers.join(' ; ')}`,
      );
    }
    const horodatage: Prisma.WorkOrderUpdateInput =
      dto.to === 'IN_PROGRESS' && !detail.startedAt
        ? { startedAt: new Date() }
        : dto.to === 'DONE'
          ? { completedAt: new Date() }
          : dto.to === 'CANCELLED'
            ? { cancelledAt: new Date() }
            : {};
    await this.prisma.workOrder.update({
      where: { id },
      data: {
        status: dto.to,
        ...horodatage,
        events: {
          create: {
            kind: 'STATUS_CHANGED',
            message:
              `${WORK_ORDER_STATUS_LABELS[detail.status]} → ${WORK_ORDER_STATUS_LABELS[dto.to]}` +
              (dto.comment ? ` — ${dto.comment}` : ''),
            byId: user.userId,
          },
        },
      },
    });
    return this.get(id, user);
  }

  async comment(
    id: string,
    message: string,
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    await this.get(id, user); // périmètre
    await this.prisma.workOrderEvent.create({
      data: { workOrderId: id, kind: 'COMMENT', message, byId: user.userId },
    });
    await this.toucher(id);
    return this.get(id, user);
  }

  async setAssignees(
    id: string,
    dto: AssigneesUpdate,
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    await this.get(id, user);
    const users = await this.prisma.user.findMany({
      where: { id: { in: dto.assigneeIds } },
    });
    if (users.length !== new Set(dto.assigneeIds).size) {
      throw new BadRequestException('Personne inconnue dans la liste');
    }
    await this.prisma.workOrder.update({
      where: { id },
      data: {
        assignees: { set: dto.assigneeIds.map((assigneeId) => ({ id: assigneeId })) },
        events: {
          create: {
            kind: 'ASSIGNED',
            message: users.length
              ? `Assigné(s) : ${users.map((u) => u.displayName).join(', ')}`
              : 'Assignation retirée',
            byId: user.userId,
          },
        },
      },
    });
    return this.get(id, user);
  }

  async upsertReport(
    id: string,
    dto: ReportUpsert,
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    await this.get(id, user);
    await this.assertVersion(id, dto.baseUpdatedAt);
    // Chaque valeur fournie doit appartenir au référentiel de SON champ (et être active)
    for (const [colonne, field] of Object.entries(REPORT_FIELDS)) {
      const valeur = dto[colonne as keyof ReportUpsert];
      if (typeof valeur === 'string') {
        const ref = await this.prisma.referenceValue.findUnique({ where: { id: valeur } });
        if (!ref || ref.field !== field || !ref.isActive) {
          throw new BadRequestException(
            `Valeur hors référentiel pour « ${BILAN_FIELD_LABELS[field]} »`,
          );
        }
      }
    }
    const donnees = {
      note: dto.note,
      doorStateId: dto.doorStateId,
      cabinPositionId: dto.cabinPositionId,
      anomalyId: dto.anomalyId,
      externalCauseId: dto.externalCauseId,
      actionTakenId: dto.actionTakenId,
      componentConcernedId: dto.componentConcernedId,
    };
    await this.prisma.interventionReport.upsert({
      where: { workOrderId: id },
      update: donnees,
      create: { workOrderId: id, ...donnees },
    });
    await this.toucher(id);
    return this.get(id, user);
  }

  async patchChecklist(
    id: string,
    itemId: string,
    dto: ChecklistPatch,
    user: AuthenticatedUser,
  ): Promise<ChecklistItemDto> {
    await this.get(id, user);
    await this.assertVersion(id, dto.baseUpdatedAt);
    const { count } = await this.prisma.checklistItem.updateMany({
      where: { id: itemId, workOrderId: id },
      data: {
        state: dto.state,
        doneById: dto.state === 'PENDING' ? null : user.userId,
        doneAt: dto.state === 'PENDING' ? null : new Date(),
      },
    });
    if (count === 0) throw new NotFoundException('Tâche inconnue');
    await this.toucher(id);
    const item = await this.prisma.checklistItem.findUniqueOrThrow({
      where: { id: itemId },
      include: { doneBy: true },
    });
    return {
      id: item.id,
      label: item.label,
      state: item.state,
      doneBy: item.doneBy ? toPersonne(item.doneBy) : null,
      doneAt: item.doneAt?.toISOString() ?? null,
    };
  }

  // ————— R3 : coûts figés —————

  /** Consommer une pièce : stock décrémenté, PRIX FIGÉ au moment T. */
  async consumePart(
    id: string,
    dto: { partId: string; quantity: number },
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    const detail = await this.get(id, user); // périmètre + existence
    if (detail.status === 'DONE' || detail.status === 'CANCELLED') {
      throw new ConflictException('OT terminé : les coûts sont figés');
    }
    const { designation, unitPrice } = await this.parts.consume(
      dto.partId,
      dto.quantity,
      id,
      user,
    );
    await this.prisma.workOrderEvent.create({
      data: {
        workOrderId: id,
        kind: 'PART_CONSUMED',
        message: `${designation} × ${dto.quantity} (${unitPrice} MAD/u, prix figé)`,
        byId: user.userId,
      },
    });
    await this.toucher(id);
    return this.get(id, user);
  }

  /** Saisir de la main-d'œuvre : TAUX FIGÉ à la saisie. */
  async addLabor(
    id: string,
    dto: { minutes: number; userId?: string; note?: string },
    user: AuthenticatedUser,
  ): Promise<WorkOrderDetail> {
    const detail = await this.get(id, user);
    if (detail.status === 'DONE' || detail.status === 'CANCELLED') {
      throw new ConflictException('OT terminé : les coûts sont figés');
    }
    const cibleId = dto.userId ?? user.userId;
    const cible = await this.prisma.user.findUnique({ where: { id: cibleId } });
    if (!cible) throw new BadRequestException('Personne inconnue');
    if (cible.hourlyRate === null) {
      throw new ConflictException(
        `Aucun taux horaire défini pour ${cible.displayName} — à renseigner dans Personnes`,
      );
    }
    await this.prisma.laborTime.create({
      data: {
        workOrderId: id,
        userId: cibleId,
        minutes: dto.minutes,
        hourlyRate: cible.hourlyRate, // figé
        note: dto.note,
      },
    });
    await this.toucher(id);
    return this.get(id, user);
  }

  // ————— mapping —————

  private toSummary(
    row: Prisma.WorkOrderGetPayload<{
      include: {
        asset: { include: { location: { include: { parent: true } } } };
        assignees: true;
      };
    }>,
  ): WorkOrderSummary {
    return {
      id: row.id,
      reference: row.reference,
      title: row.title,
      type: row.type,
      status: row.status,
      priority: row.priority,
      assetId: row.assetId,
      assetReference: row.asset.reference,
      siteName: row.asset.location.parent?.name ?? row.asset.location.name,
      dueDate: row.dueDate?.toISOString() ?? null,
      assignees: row.assignees.map(toPersonne),
      createdAt: row.createdAt.toISOString(),
    };
  }

  private toDetail(row: DetailRow): WorkOrderDetail {
    const report = row.report;
    const blockers: string[] = [];
    if (row.checklist.some((c) => c.state === 'PENDING')) {
      blockers.push('la checklist est incomplète : traitez chaque tâche (Fait ou N-A)');
    }
    const manquants = REQUIRED_BILAN_FIELDS.filter((field) => {
      const parChamp: Record<BilanField, string | null | undefined> = {
        DOOR_STATE: report?.doorStateId,
        CABIN_POSITION: report?.cabinPositionId,
        ANOMALY: report?.anomalyId,
        EXTERNAL_CAUSE: report?.externalCauseId,
        ACTION_TAKEN: report?.actionTakenId,
        COMPONENT_CONCERNED: report?.componentConcernedId,
      };
      return !parChamp[field];
    });
    if (manquants.length) {
      blockers.push(
        `bilan incomplet : ${manquants.map((f) => `« ${BILAN_FIELD_LABELS[f]} »`).join(', ')}`,
      );
    }
    const versValeur = (v: { id: string; label: string } | null) =>
      v ? { id: v.id, label: v.label } : null;
    // Coûts figés : chaque ligne porte le prix/taux du moment T
    const partsCosts = row.stockMovements.map((m) => ({
      id: m.id,
      designation: m.part.designation,
      quantity: -m.quantity,
      unitPrice: Number(m.unitPrice ?? 0),
      total: -m.quantity * Number(m.unitPrice ?? 0),
    }));
    const laborCosts = row.laborTimes.map((l) => ({
      id: l.id,
      displayName: l.user.displayName,
      minutes: l.minutes,
      hourlyRate: Number(l.hourlyRate),
      total: Math.round((l.minutes / 60) * Number(l.hourlyRate) * 100) / 100,
      note: l.note,
    }));
    return {
      ...this.toSummary(row),
      description: row.description,
      locationName: row.asset.location.name,
      startedAt: row.startedAt?.toISOString() ?? null,
      completedAt: row.completedAt?.toISOString() ?? null,
      cancelledAt: row.cancelledAt?.toISOString() ?? null,
      createdBy: row.createdBy ? toPersonne(row.createdBy) : null,
      request: row.request
        ? {
            id: row.request.id,
            reference: row.request.reference,
            requesterLabel:
              row.request.requestedBy?.displayName ??
              row.request.requesterName ??
              'Portail',
          }
        : null,
      events: row.events.map((e) => ({
        id: e.id,
        kind: e.kind,
        message: e.message,
        by: e.by ? toPersonne(e.by) : null,
        createdAt: e.createdAt.toISOString(),
      })),
      checklist: row.checklist.map((c) => ({
        id: c.id,
        label: c.label,
        state: c.state,
        doneBy: c.doneBy ? toPersonne(c.doneBy) : null,
        doneAt: c.doneAt?.toISOString() ?? null,
      })),
      report: report
        ? {
            note: report.note,
            doorState: versValeur(report.doorState),
            cabinPosition: versValeur(report.cabinPosition),
            anomaly: versValeur(report.anomaly),
            externalCause: versValeur(report.externalCause),
            actionTaken: versValeur(report.actionTaken),
            componentConcerned: versValeur(report.componentConcerned),
          }
        : null,
      costs: {
        parts: partsCosts,
        labor: laborCosts,
        total:
          Math.round(
            (partsCosts.reduce((s, p) => s + p.total, 0) +
              laborCosts.reduce((s, l) => s + l.total, 0)) *
              100,
          ) / 100,
      },
      allowedTransitions: WORK_ORDER_TRANSITIONS[row.status as WorkOrderStatus],
      closureBlockers: blockers,
      updatedAt: row.updatedAt.toISOString(),
    };
  }

  /** Verrou optimiste (D2 mobile) : si l'appelant fournit la version lue et
   *  que l'OT a changé depuis, 409 avec le contexte (qui, quand) — rien
   *  n'est écrasé en silence. Sans version fournie (web), comportement
   *  inchangé. */
  private async assertVersion(id: string, baseUpdatedAt?: string): Promise<void> {
    if (!baseUpdatedAt) return;
    const wo = await this.prisma.workOrder.findUniqueOrThrow({
      where: { id },
      select: {
        updatedAt: true,
        events: {
          orderBy: { createdAt: 'desc' },
          take: 1,
          include: { by: true },
        },
      },
    });
    if (wo.updatedAt.getTime() > new Date(baseUpdatedAt).getTime()) {
      const dernier = wo.events[0];
      const qui = dernier?.by?.displayName ?? 'quelqu’un';
      const quand = new Intl.DateTimeFormat('fr-FR', {
        hour: '2-digit',
        minute: '2-digit',
      }).format(wo.updatedAt);
      throw new ConflictException(
        `Conflit de version : cet OT a été modifié par ${qui} à ${quand} — votre saisie n'a pas été appliquée.`,
      );
    }
  }

  /** Toute écriture « secondaire » (coche, bilan, commentaire, coûts) fait
   *  avancer la version de l'OT — sinon le verrou D2 ne verrait rien. */
  private async toucher(id: string): Promise<void> {
    await this.prisma.workOrder.update({ where: { id }, data: { updatedAt: new Date() } });
  }
}
