import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import type {
  FitPresetRuleInput,
  FitPresetCreateInput,
  FitPresetListQuery,
  FitPresetUpdateInput,
} from '@ali-ismail/contracts';
import {
  fitFieldFamiliesForKey,
  isFitAdjustableField,
} from '@ali-ismail/contracts';
import { FitAdjustmentType, FitPresetApplicability, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NumberingService } from '../numbering/numbering.service';
import { AuditService } from '../audit/audit.service';
import { applyFitRules, type FitRuleLike } from './fit.logic';

type RuleValidationInput = Pick<
  FitPresetRuleInput,
  'fieldKey' | 'adjustmentType' | 'adjustmentValue' | 'applicability' | 'isActive'
>;

const LEGACY_FIT_REMEDIATION_MESSAGE =
  'Legacy measurement rules must be replaced or removed in Fit Presets before this preset can be used.';

function isRegularPreset(name: string, code: string) {
  return name.trim().toUpperCase() === 'REGULAR' || code.trim().toUpperCase() === 'REGULAR';
}

/**
 * Domain validation shared by create, update and preview so direct API callers
 * cannot bypass the scope-aware field selector.
 */
export function validateFitPresetRules(
  presetApplicability: 'KS' | 'CP' | 'BOTH',
  rules: readonly RuleValidationInput[],
  identity?: { name: string; code: string },
) {
  const ruleKeys = new Set<string>();

  for (const rule of rules) {
    const families = fitFieldFamiliesForKey(rule.fieldKey);
    if (families.length === 0) {
      throw new BadRequestException(`Field "${rule.fieldKey}" is not fit-adjustable`);
    }

    const ruleApplicability = rule.applicability ?? 'BOTH';
    if (ruleApplicability === 'BOTH') {
      if (!isFitAdjustableField('KS', rule.fieldKey) || !isFitAdjustableField('CP', rule.fieldKey)) {
        throw new BadRequestException(
          `Field "${rule.fieldKey}" must use its exact KS or CP family scope`,
        );
      }
    } else if (!isFitAdjustableField(ruleApplicability, rule.fieldKey)) {
      throw new BadRequestException(
        `Field "${rule.fieldKey}" is not valid for ${ruleApplicability} fit presets`,
      );
    }

    if (
      presetApplicability !== 'BOTH' &&
      ruleApplicability !== presetApplicability
    ) {
      throw new BadRequestException(
        `${ruleApplicability} field "${rule.fieldKey}" is outside the ${presetApplicability} preset scope`,
      );
    }

    const duplicateKey = `${ruleApplicability}:${rule.fieldKey}`;
    if (ruleKeys.has(duplicateKey)) {
      throw new BadRequestException(
        `Only one adjustment is allowed for field "${rule.fieldKey}"`,
      );
    }
    ruleKeys.add(duplicateKey);
  }

  if (
    identity &&
    isRegularPreset(identity.name, identity.code) &&
    rules.some((rule) => rule.isActive !== false)
  ) {
    throw new BadRequestException('Regular is a zero-adjustment preset and cannot have active rules');
  }
}

type StoredPresetValidationInput = {
  applicability: 'KS' | 'CP' | 'BOTH';
  name: string;
  code: string;
  rules: ReadonlyArray<{
    fieldKey: string;
    adjustmentType: FitAdjustmentType;
    adjustmentValue: { toString(): string };
    applicability: FitPresetApplicability;
    isActive: boolean;
  }>;
};

export function fitPresetRemediationMessage(
  preset: StoredPresetValidationInput,
): string | null {
  try {
    validateFitPresetRules(
      preset.applicability,
      preset.rules.map((rule) => ({
        fieldKey: rule.fieldKey,
        adjustmentType: rule.adjustmentType,
        adjustmentValue: rule.adjustmentValue.toString(),
        applicability: rule.applicability,
        isActive: rule.isActive,
      })),
      { name: preset.name, code: preset.code },
    );
    return null;
  } catch (error) {
    if (error instanceof BadRequestException) return LEGACY_FIT_REMEDIATION_MESSAGE;
    throw error;
  }
}

@Injectable()
export class FitService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly numbering: NumberingService,
    private readonly audit: AuditService,
  ) {}

  private readonly include = {
    rules: { orderBy: { sortOrder: 'asc' } },
  } satisfies Prisma.FitPresetInclude;

  async list(organizationId: string, query: FitPresetListQuery) {
    const where: Prisma.FitPresetWhereInput = {
      organizationId,
      ...(query.includeArchived ? {} : { archivedAt: null }),
      ...(query.applicability
        ? {
            applicability: {
              in:
                query.applicability === 'BOTH'
                  ? [FitPresetApplicability.BOTH]
                  : [query.applicability as FitPresetApplicability, FitPresetApplicability.BOTH],
            },
          }
        : {}),
      ...(query.q ? { OR: [{ name: { contains: query.q } }, { code: { contains: query.q } }] } : {}),
    };
    const presets = await this.prisma.fitPreset.findMany({
      where,
      include: this.include,
      orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
    });
    const annotated = presets.map((preset) => {
      const remediationMessage = fitPresetRemediationMessage(preset);
      return {
        ...preset,
        requiresRemediation: remediationMessage !== null,
        remediationMessage,
      };
    });
    return query.includeArchived
      ? annotated
      : annotated.filter((preset) => !preset.requiresRemediation);
  }

  async get(organizationId: string, id: string) {
    const preset = await this.prisma.fitPreset.findFirst({
      where: { id, organizationId },
      include: this.include,
    });
    if (!preset) throw new NotFoundException('Fit preset not found');
    return preset;
  }

  private slugCode(name: string) {
    return name
      .toUpperCase()
      .replace(/[^A-Z0-9]+/g, '_')
      .replace(/^_+|_+$/g, '')
      .slice(0, 60);
  }

  async create(organizationId: string, dto: FitPresetCreateInput, actorUserId: string) {
    const code = (dto.code?.trim() || this.slugCode(dto.name)) || 'PRESET';
    if (dto.name.trim().toUpperCase() === 'REGULAR' && code.toUpperCase() !== 'REGULAR') {
      throw new BadRequestException('The Regular preset must use the REGULAR code');
    }
    validateFitPresetRules(
      dto.applicability ?? 'BOTH',
      dto.rules ?? [],
      { name: dto.name, code },
    );
    const existingCode = await this.prisma.fitPreset.findFirst({
      where: { organizationId, code },
      select: { id: true },
    });
    if (existingCode) throw new BadRequestException(`Fit preset code "${code}" already exists`);

    const created = await this.prisma.$transaction(async (tx) => {
      const preset = await tx.fitPreset.create({
        data: {
          organizationId,
          code,
          name: dto.name.trim(),
          applicability: (dto.applicability ?? 'BOTH') as FitPresetApplicability,
          isActive: dto.isActive ?? undefined,
          sortOrder: dto.sortOrder ?? undefined,
        },
      });
      if (dto.rules?.length) {
        await tx.fitPresetRule.createMany({
          data: dto.rules.map((r, idx) => ({
            organizationId,
            fitPresetId: preset.id,
            fieldKey: r.fieldKey,
            adjustmentType: r.adjustmentType as FitAdjustmentType,
            adjustmentValue: new Prisma.Decimal(r.adjustmentValue),
            applicability: (r.applicability ?? 'BOTH') as FitPresetApplicability,
            isActive: r.isActive ?? true,
            sortOrder: r.sortOrder ?? idx,
          })),
        });
      }
      return tx.fitPreset.findUniqueOrThrow({ where: { id: preset.id }, include: this.include });
    });

    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'fit_preset.create',
      entityType: 'fit_preset',
      entityId: created.id,
      after: created,
    });
    return created;
  }

  async update(
    organizationId: string,
    id: string,
    dto: FitPresetUpdateInput,
    actorUserId: string,
  ) {
    const existing = await this.get(organizationId, id);
    const effectiveName = dto.name?.trim() ?? existing.name;
    const effectiveApplicability =
      (dto.applicability ?? existing.applicability) as 'KS' | 'CP' | 'BOTH';
    if (
      effectiveName.toUpperCase() === 'REGULAR' &&
      existing.code.toUpperCase() !== 'REGULAR'
    ) {
      throw new BadRequestException('Only the REGULAR preset may use the Regular name');
    }
    const effectiveRules: RuleValidationInput[] = dto.rules
      ? dto.rules
      : existing.rules.map((rule) => ({
          fieldKey: rule.fieldKey,
          adjustmentType: rule.adjustmentType,
          adjustmentValue: rule.adjustmentValue.toString(),
          applicability: rule.applicability,
          isActive: rule.isActive,
        }));
    try {
      validateFitPresetRules(
        effectiveApplicability,
        effectiveRules,
        { name: effectiveName, code: existing.code },
      );
    } catch (error) {
      if (!dto.rules && error instanceof BadRequestException) {
        throw new BadRequestException(LEGACY_FIT_REMEDIATION_MESSAGE);
      }
      throw error;
    }

    const updated = await this.prisma.$transaction(async (tx) => {
      await tx.fitPreset.update({
        where: { id },
        data: {
          name: dto.name?.trim() ?? undefined,
          applicability: dto.applicability
            ? (dto.applicability as FitPresetApplicability)
            : undefined,
          isActive: dto.isActive === undefined ? undefined : dto.isActive,
          sortOrder: dto.sortOrder ?? undefined,
        },
      });
      // Rule replacement: only when rules explicitly supplied.
      if (dto.rules) {
        await tx.fitPresetRule.deleteMany({ where: { fitPresetId: id } });
        if (dto.rules.length) {
          await tx.fitPresetRule.createMany({
            data: dto.rules.map((r, idx) => ({
              organizationId,
              fitPresetId: id,
              fieldKey: r.fieldKey,
              adjustmentType: r.adjustmentType as FitAdjustmentType,
              adjustmentValue: new Prisma.Decimal(r.adjustmentValue),
              applicability: (r.applicability ?? 'BOTH') as FitPresetApplicability,
              isActive: r.isActive ?? true,
              sortOrder: r.sortOrder ?? idx,
            })),
          });
        }
      }
      return tx.fitPreset.findUniqueOrThrow({ where: { id }, include: this.include });
    });

    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'fit_preset.update',
      entityType: 'fit_preset',
      entityId: id,
      before: existing,
      after: updated,
    });
    return updated;
  }

  async archive(organizationId: string, id: string, actorUserId: string) {
    const existing = await this.get(organizationId, id);
    const updated = await this.prisma.fitPreset.update({
      where: { id },
      data: { archivedAt: new Date(), isActive: false },
      include: this.include,
    });
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'fit_preset.archive',
      entityType: 'fit_preset',
      entityId: id,
      before: existing,
      after: updated,
    });
    return updated;
  }

  async restore(organizationId: string, id: string, actorUserId: string) {
    const existing = await this.get(organizationId, id);
    const remediationMessage = fitPresetRemediationMessage(existing);
    if (remediationMessage) throw new BadRequestException(remediationMessage);
    const updated = await this.prisma.fitPreset.update({
      where: { id },
      data: { archivedAt: null, isActive: true },
      include: this.include,
    });
    await this.audit.write({
      organizationId,
      actorUserId,
      action: 'fit_preset.restore',
      entityType: 'fit_preset',
      entityId: id,
      before: existing,
      after: updated,
    });
    return updated;
  }

  /** Rules for a preset (list). */
  async listRules(organizationId: string, presetId: string) {
    await this.get(organizationId, presetId);
    return this.prisma.fitPresetRule.findMany({
      where: { organizationId, fitPresetId: presetId },
      orderBy: { sortOrder: 'asc' },
    });
  }

  /**
   * Compute the final garment values for a set of actual body values by applying
   * a preset's rules. Returns actual + final maps and the fit snapshot. Pure /
   * side-effect free. Regular ("no preset") returns final = actual.
   */
  async preview(
    organizationId: string,
    input: {
      type: 'KS' | 'CP';
      fitPresetId?: string | null;
      actualFields: Record<string, string | null | undefined>;
    },
  ) {
    let rules: FitRuleLike[] = [];
    let presetSnapshot: {
      id: string;
      code: string;
      name: string;
      rules: Array<{
        fieldKey: string;
        adjustmentType: FitAdjustmentType;
        adjustmentValue: string;
        applicability: string;
      }>;
    } | null = null;

    if (input.fitPresetId) {
      const preset = await this.get(organizationId, input.fitPresetId);
      // Defect fix: reject archived or inactive presets at apply/preview time.
      // Historical reads (GET /fit-presets/:id) remain valid for audit purposes.
      if (preset.archivedAt != null) {
        throw new BadRequestException('Fit preset is archived and cannot be applied');
      }
      if (!preset.isActive) {
        throw new BadRequestException('Fit preset is inactive and cannot be applied');
      }
      const remediationMessage = fitPresetRemediationMessage(preset);
      if (remediationMessage) throw new BadRequestException(remediationMessage);
      const activeRules = preset.rules.filter((r) => r.isActive);
      if (preset.applicability !== 'BOTH' && preset.applicability !== input.type) {
        throw new BadRequestException(
          `Fit preset is not applicable to ${input.type} measurements`,
        );
      }
      validateFitPresetRules(
        preset.applicability,
        activeRules.map((rule) => ({
          fieldKey: rule.fieldKey,
          adjustmentType: rule.adjustmentType,
          adjustmentValue: rule.adjustmentValue.toString(),
          applicability: rule.applicability,
          isActive: rule.isActive,
        })),
        { name: preset.name, code: preset.code },
      );
      rules = activeRules.map((r) => ({
        fieldKey: r.fieldKey,
        adjustmentType: r.adjustmentType,
        adjustmentValue: r.adjustmentValue,
        applicability: r.applicability,
      }));
      presetSnapshot = {
        id: preset.id,
        code: preset.code,
        name: preset.name,
        rules: activeRules.map((r) => ({
          fieldKey: r.fieldKey,
          adjustmentType: r.adjustmentType,
          adjustmentValue: r.adjustmentValue.toString(),
          applicability: r.applicability,
        })),
      };
    }

    const actualSnapshot = { type: input.type, fields: { ...input.actualFields } };
    const finalFields = applyFitRules(input.actualFields, rules, input.type);
    const finalSnapshot = { type: input.type, fields: finalFields };

    return {
      type: input.type,
      fitPresetId: input.fitPresetId ?? null,
      actualSnapshotJson: actualSnapshot,
      finalSnapshotJson: finalSnapshot,
      fitSnapshotJson: presetSnapshot,
    };
  }
}
