import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { RecycleBinRepository } from './recycle-bin.repository';
import { RecycleBinQuery } from './recycle-bin.dto';
import {
  deletedRecordTypes,
  type DeletedRecordType,
} from './recycle-bin.registry';

@Injectable()
export class RecycleBinService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly records: RecycleBinRepository,
  ) {}

  parseType(type: string): DeletedRecordType {
    if (!deletedRecordTypes.includes(type as DeletedRecordType))
      throw new BadRequestException('Unsupported record type');
    return type as DeletedRecordType;
  }

  list(query: RecycleBinQuery) {
    if (query.type !== 'all') this.parseType(query.type);
    if (
      query.deletedFrom &&
      query.deletedTo &&
      new Date(query.deletedFrom) > new Date(query.deletedTo)
    ) {
      throw new BadRequestException(
        'The start date must not be after the end date',
      );
    }
    return this.records.list(query);
  }

  async detail(type: DeletedRecordType, id: number) {
    const record = await this.records.find(type, id);
    if (!record?.deletedAt)
      throw new NotFoundException('Deleted record not found');
    return {
      ...record,
      parents: await this.records.parents(record),
      assignments: await this.records.assignments(type, id),
      relatedCounts: await this.records.relatedCounts(type, id),
    };
  }

  async restore(
    type: DeletedRecordType,
    id: number,
    actorId: number,
    language = 'en',
  ) {
    const kh = /^(km|kh)/i.test(language);
    try {
      return await this.prisma.$transaction(
        async (tx) => {
          await this.records.lock(type, tx);
          const record = await this.records.find(type, id, tx);
          if (!record)
            throw new NotFoundException(
              kh ? 'រកមិនឃើញទិន្នន័យ' : 'Record not found',
            );
          if (!record.deletedAt)
            throw new ConflictException(
              kh
                ? 'ទិន្នន័យនេះត្រូវបានស្ដាររួចហើយ'
                : 'This record has already been restored',
            );

          if (await this.records.hasDuplicate(record, tx)) {
            if (type === 'progress-reports') {
              const period = `${String(record.details.semester)} / ${String(record.details.year)}`;
              throw new ConflictException(
                kh
                  ? `មានរបាយការណ៍សកម្មសម្រាប់ ${period} រួចហើយ។ សូមលុបរបាយការណ៍នោះជាមុន ប្រសិនបើអ្នកចង់ស្ដាររបាយការណ៍នេះ។`
                  : `An active report for ${String(record.details.semester)} in ${String(record.details.year)} already exists. Delete that report first if you want to restore this one.`,
              );
            }
            throw new ConflictException(
              kh
                ? 'មិនអាចស្ដារបានទេ ព្រោះមានទិន្នន័យសកម្មដូចគ្នារួចហើយ'
                : 'Cannot restore because a matching active record already exists',
            );
          }
          const parents = await this.records.parents(record, tx, true);
          const deletedParent = parents.find((parent) => parent.deleted);
          if (deletedParent)
            throw new ConflictException(
              kh
                ? `សូមស្ដារទិន្នន័យដែលពាក់ព័ន្ធ «${deletedParent.title}» (#${deletedParent.id}) ជាមុន។`
                : `Restore the related ${deletedParent.type} record "${deletedParent.title}" (#${deletedParent.id}) first.`,
            );

          await this.records.restore(record, tx);
          const actor = await tx.user.findUniqueOrThrow({
            where: { id: actorId },
            select: {
              name: true,
              email: true,
              roles: {
                where: { role: { deletedAt: null } },
                select: { role: { select: { name: true } } },
              },
            },
          });
          // The restoration and its audit entry commit together.
          await tx.activityLog.create({
            data: {
              actorId,
              actorName: actor.name ?? actor.email,
              actorRoles: actor.roles.map(({ role }) => role.name),
              module: type,
              action: 'RESTORE',
              recordReference: `${type}/${id}`,
              recordId: id,
              recordLabel: record.title?.slice(0, 255),
              method: 'POST',
              endpoint: `/admin/recycle-bin/${type}/${id}/restore`,
              changes: {
                deletedAt: { from: record.deletedAt.toISOString(), to: null },
              },
              outcome: 'SUCCESS',
              statusCode: 200,
            },
          });
          return {
            success: true,
            data: { id, type },
            message: kh
              ? 'បានស្ដារទិន្នន័យដោយជោគជ័យ'
              : 'Record restored successfully',
          };
        },
        { timeout: 15000 },
      );
    } catch (error) {
      // Concurrent operations must produce an actionable conflict, never a raw SQL error.
      const code = (error as { code?: string })?.code;
      if (
        code === 'P2002' ||
        code === 'P2034' ||
        (code === 'P2010' &&
          ['23505', '40P01', '40001'].includes(
            String((error as { meta?: { code?: string } }).meta?.code),
          ))
      ) {
        throw new ConflictException(
          kh
            ? 'ទិន្នន័យបានផ្លាស់ប្ដូរ។ សូមផ្ទុកឡើងវិញ ហើយព្យាយាមម្ដងទៀត។'
            : 'A conflicting change occurred. Refresh the page and try again.',
        );
      }
      throw error;
    }
  }

  async permanentlyDelete(
    type: DeletedRecordType,
    id: number,
    actorId: number,
    language = 'en',
  ) {
    const kh = /^(km|kh)/i.test(language);
    try {
      return await this.prisma.$transaction(
        async (tx) => {
          await this.records.lock(type, tx);
          // The row lock also prevents new foreign-key links during this check.
          await this.records.lockRecord(type, id, tx);
          const record = await this.records.find(type, id, tx);
          if (!record)
            throw new NotFoundException(
              kh ? 'រកមិនឃើញទិន្នន័យ' : 'Record not found',
            );
          if (!record.deletedAt)
            throw new ConflictException(
              kh
                ? 'អាចលុបជាអចិន្ត្រៃយ៍បានតែទិន្នន័យក្នុងធុងសំរាមប៉ុណ្ណោះ។'
                : 'Only records already in Recycle Bin can be permanently deleted.',
            );
          const sharedReportData =
            type === 'progress-reports' &&
            (await this.records.hasSharedProgressReportData(id, tx));
          if (sharedReportData)
            throw new ConflictException(
              kh
                ? 'មិនអាចលុបជាអចិន្ត្រៃយ៍បានទេ ព្រោះមានទិន្នន័យផ្សេងទៀតភ្ជាប់នឹងទិន្នន័យនេះ។ សូមដកការភ្ជាប់ជាមុន។'
                : 'Cannot permanently delete this record while other records reference it. Remove those links first.',
            );
          // Capture the actor before removing role memberships or a deleted user.
          const actor = await tx.user.findUniqueOrThrow({
            where: { id: actorId },
            select: {
              name: true,
              email: true,
              roles: {
                where: { role: { deletedAt: null } },
                select: { role: { select: { name: true } } },
              },
            },
          });
          await this.records.removeOwnedDataAndLinks(record, tx);
          const blockingLink = await this.records.findBlockingLink(record, tx);
          if (blockingLink)
            throw new ConflictException(
              kh
                ? `មិនអាចលុបជាអចិន្ត្រៃយ៍បានទេ ព្រោះទិន្នន័យរួមនៅតែភ្ជាប់តាម ${blockingLink.table}.${blockingLink.column}។ សូមប្ដូរការភ្ជាប់ដែលចាំបាច់ជាមុន។`
                : `Cannot permanently delete this record because a shared record still references it (${blockingLink.table}.${blockingLink.column}). Reassign the required link first.`,
            );
          await this.records.permanentlyDelete(record, tx);
          // Keep the audit entry even after the original record no longer exists.
          await tx.activityLog.create({
            data: {
              actorId,
              actorName: actor.name ?? actor.email,
              actorRoles: actor.roles.map(({ role }) => role.name),
              module: type,
              action: 'PERMANENT_DELETE',
              recordReference: `${type}/${id}`,
              recordId: id,
              recordLabel: record.title?.slice(0, 255),
              method: 'DELETE',
              endpoint: `/admin/recycle-bin/${type}/${id}`,
              changes: { record: { from: record.title, to: null } },
              outcome: 'SUCCESS',
              statusCode: 200,
            },
          });
          return {
            message: kh
              ? 'បានលុបទិន្នន័យជាអចិន្ត្រៃយ៍ដោយជោគជ័យ'
              : 'Record permanently deleted successfully',
            id,
            type,
          };
        },
        { timeout: 15000 },
      );
    } catch (error) {
      const code = (error as { code?: string })?.code;
      const sqlCode = String(
        (error as { meta?: { code?: string } })?.meta?.code,
      );
      if (
        code === 'P2003' ||
        code === 'P2034' ||
        (code === 'P2010' && ['23503', '40P01', '40001'].includes(sqlCode))
      ) {
        throw new ConflictException(
          kh
            ? 'ទិន្នន័យមានការភ្ជាប់ ឬបានផ្លាស់ប្ដូរ។ សូមផ្ទុកឡើងវិញ ហើយព្យាយាមម្ដងទៀត។'
            : 'This record has linked data or has changed. Refresh the page and try again.',
        );
      }
      throw error;
    }
  }
}
