import {
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type {
  PreventiveGenerationResult,
  PreventiveStatus,
  TaskTemplateCreate,
  TaskTemplateDto,
  TaskTemplatesResponse,
  TaskTemplateUpdate,
} from '@siop/shared';
import type { AuthenticatedUser } from '../auth/current-user.decorator';
import { PrismaService } from '../prisma/prisma.service';

const templateInclude = {
  componentType: true,
} satisfies Prisma.TaskTemplateInclude;

type TemplateRow = Prisma.TaskTemplateGetPayload<{ include: typeof templateInclude }>;

const MOIS_FR = [
  'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
  'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre',
];

/** « AAAA-MM » → index absolu de mois. */
const indexMois = (period: string): number => {
  const [annee, mois] = period.split('-').map(Number);
  return annee! * 12 + (mois! - 1);
};

@Injectable()
export class PreventiveService {
  private readonly logger = new Logger(PreventiveService.name);

  constructor(private readonly prisma: PrismaService) {}

  // ————— Gabarits —————

  async listTemplates(): Promise<TaskTemplatesResponse> {
    const rows = await this.prisma.taskTemplate.findMany({
      include: templateInclude,
      orderBy: [{ periodMonths: 'asc' }, { label: 'asc' }],
    });
    return { templates: rows.map((t) => this.toDto(t)) };
  }

  async createTemplate(dto: TaskTemplateCreate): Promise<TaskTemplateDto> {
    try {
      const created = await this.prisma.taskTemplate.create({
        data: dto,
        include: templateInclude,
      });
      return this.toDto(created);
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Ce libellé de tâche existe déjà');
      }
      throw e;
    }
  }

  async updateTemplate(id: string, dto: TaskTemplateUpdate): Promise<TaskTemplateDto> {
    try {
      const updated = await this.prisma.taskTemplate.update({
        where: { id },
        data: dto,
        include: templateInclude,
      });
      return this.toDto(updated);
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
        throw new NotFoundException('Gabarit inconnu');
      }
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Ce libellé de tâche existe déjà');
      }
      throw e;
    }
  }

  // ————— Génération mensuelle (IDEMPOTENTE) —————

  /**
   * Une grille par appareil sous contrat et par mois — l'unicité
   * [assetId, periodKey] est en BASE : regénérer ne double jamais rien,
   * même en cas d'appels concurrents.
   * Tâches dues : périodicité ancrée sur la mise en service (à défaut, tout
   * mois) ; appareil sans historique préventif → « premier contrôle » (tout).
   */
  async generate(
    month: string | undefined,
    user: AuthenticatedUser,
  ): Promise<PreventiveGenerationResult> {
    const period = month ?? new Date().toISOString().slice(0, 7);
    const [annee, mois] = period.split('-').map(Number);
    const dernierJour = new Date(Date.UTC(annee!, mois!, 0, 23, 59, 59));
    const libelleMois = `${MOIS_FR[mois! - 1]} ${annee}`;

    const [assets, templates] = await Promise.all([
      this.prisma.asset.findMany({
        where: { underContract: true },
        include: { _count: { select: { workOrders: { where: { type: 'PREVENTIVE' } } } } },
      }),
      this.prisma.taskTemplate.findMany({ where: { isActive: true } }),
    ]);

    let created = 0;
    let skipped = 0;
    let firstControls = 0;
    const references: string[] = [];

    for (const asset of assets) {
      const premierControle = asset._count.workOrders === 0;
      const dues = premierControle
        ? templates
        : templates.filter((t) => {
            if (t.periodMonths === 1) return true;
            const ancre = asset.commissionedAt
              ? asset.commissionedAt.getUTCFullYear() * 12 + asset.commissionedAt.getUTCMonth()
              : indexMois(period);
            return (indexMois(period) - ancre) % t.periodMonths === 0;
          });
      if (dues.length === 0) {
        skipped++;
        continue;
      }
      let issue: 'créé' | 'sauté' | null = null;
      for (let essai = 0; essai < 4 && issue === null; essai++) {
        try {
          const ot = await this.prisma.workOrder.create({
            data: {
              reference: await this.nextReference(),
              title: premierControle
                ? `Premier contrôle — ${libelleMois}`
                : `Grille du mois — ${libelleMois}`,
              type: 'PREVENTIVE',
              priority: 'LOW',
              assetId: asset.id,
              periodKey: period,
              dueDate: dernierJour,
              createdById: user.userId,
              checklist: {
                create: dues.map((t) => ({ label: t.label, templateId: t.id })),
              },
              events: {
                create: {
                  kind: 'GENERATED',
                  message: `Généré (${premierControle ? 'premier contrôle' : 'grille mensuelle'} · ${dues.length} tâches)`,
                  byId: user.userId,
                },
              },
            },
            select: { reference: true },
          });
          created++;
          if (premierControle) firstControls++;
          references.push(ot.reference);
          issue = 'créé';
        } catch (e) {
          if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
            const cible = (e.meta?.target as string[] | string | undefined) ?? '';
            if (String(cible).includes('periodKey')) {
              issue = 'sauté'; // grille déjà en place : idempotence
            }
            // sinon : collision de référence (course avec une création d'OT)
            // → on recalcule et on retente
          } else if (
            e instanceof Prisma.PrismaClientKnownRequestError &&
            e.code === 'P2003'
          ) {
            issue = 'sauté'; // appareil supprimé entre lecture et écriture
          } else {
            throw e;
          }
        }
      }
      if (issue !== 'créé') skipped++;
    }
    this.logger.log(
      `Préventif ${period} : ${created} générés, ${skipped} déjà en place, ${firstControls} premiers contrôles.`,
    );
    return { month: period, created, skipped, firstControls, references };
  }

  async status(): Promise<PreventiveStatus> {
    const period = new Date().toISOString().slice(0, 7);
    const [assetsUnderContract, grilles] = await Promise.all([
      this.prisma.asset.count({ where: { underContract: true } }),
      this.prisma.workOrder.findMany({
        where: { periodKey: period },
        select: { status: true, dueDate: true, title: true },
      }),
    ]);
    const maintenant = new Date();
    return {
      month: period,
      assetsUnderContract,
      generated: grilles.length,
      done: grilles.filter((g) => g.status === 'DONE').length,
      late: grilles.filter(
        (g) => g.status !== 'DONE' && g.status !== 'CANCELLED' && g.dueDate! < maintenant,
      ).length,
      firstControls: grilles.filter((g) => g.title.startsWith('Premier contrôle')).length,
    };
  }

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

  private toDto(row: TemplateRow): TaskTemplateDto {
    return {
      id: row.id,
      label: row.label,
      periodMonths: row.periodMonths,
      isRegulatory: row.isRegulatory,
      isActive: row.isActive,
      componentTypeId: row.componentTypeId,
      componentTypeName: row.componentType?.name ?? null,
    };
  }
}
