import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PlateMaster, Prisma } from '../generated/prisma/index';
import { PrismaService } from '../prisma/prisma.service';
import { clean, normalizeStatus } from '../holder-master/holder-master-import';
import {
  PlateImportRow,
  parsePlateImport,
  plateCsvHeaders,
} from './plate-master-import';

type PlateQuery = {
  page?: string;
  pageSize?: string;
  search?: string;
  status?: string;
  holderCode?: string;
  brakeName?: string;
  sortBy?: string;
  sortOrder?: string;
};

type PlateWriteDto = {
  sourceId?: number | string | null;
  holderCode?: string;
  plateName?: string;
  brakeName?: string;
  plateCode?: string;
  brakeType?: string;
  brakeTypeCode?: string;
  loadCarryingType?: string;
  loadCarryingCode?: string;
  mountingType?: string;
  mountingCode?: string;
  fittingSize?: string;
  fittingSizeCode?: string;
  bearingType?: string;
  bearingCode?: string;
  fittingType?: string;
  fittingCode?: string;
  yokeName?: string | null;
  yokeCode?: string | null;
  status?: string;
};

const sortColumns = new Set([
  'holderCode',
  'plateName',
  'brakeName',
  'plateCode',
  'mountingType',
  'bearingType',
  'status',
  'updatedAt',
]);

@Injectable()
export class PlateMasterService {
  constructor(private readonly prisma: PrismaService) {}

  async list(query: PlateQuery) {
    const page = this.toPositiveNumber(query.page, 1);
    const pageSize = Math.min(this.toPositiveNumber(query.pageSize, 10), 100);
    const where = this.buildWhere(query);
    const sortBy = sortColumns.has(query.sortBy || '')
      ? (query.sortBy as keyof PlateMaster)
      : 'plateCode';
    const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
    const orderBy = {
      [sortBy]: sortOrder,
    } as Prisma.PlateMasterOrderByWithRelationInput;

    const [total, items, statuses] = await Promise.all([
      this.prisma.plateMaster.count({ where }),
      this.prisma.plateMaster.findMany({
        where,
        orderBy,
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
      this.getStatusOptions(),
    ]);

    return {
      items,
      meta: {
        total,
        page,
        pageSize,
        totalPages: Math.max(1, Math.ceil(total / pageSize)),
      },
      filterOptions: statuses,
    };
  }

  async getHolderCodeOptions() {
    const items = await this.prisma.plateMaster.findMany({
      distinct: ['holderCode'],
      orderBy: { holderCode: 'asc' },
      select: { holderCode: true },
    });

    return items.map((item) => item.holderCode);
  }

  async getBrakeNameOptions() {
    const items = await this.prisma.plateMaster.findMany({
      distinct: ['brakeName'],
      orderBy: { brakeName: 'asc' },
      select: { brakeName: true },
    });

    return items.map((item) => item.brakeName);
  }

  async create(dto: PlateWriteDto) {
    const data = this.validateWrite(dto);
    await this.ensureProductTagCodes(data, `Plate code "${data.plateCode}"`);
    return this.prisma.plateMaster.create({ data });
  }

  async update(id: string, dto: PlateWriteDto) {
    await this.ensureExists(id);
    const data = this.validateWrite(dto);
    await this.ensureProductTagCodes(data, `Plate code "${data.plateCode}"`);
    return this.prisma.plateMaster.update({
      where: { id },
      data,
    });
  }

  async delete(id: string) {
    await this.ensureExists(id);
    await this.prisma.plateMaster.delete({ where: { id } });
    return { deleted: true };
  }

  async importFile(buffer: Buffer) {
    const text = buffer.toString('utf8').replace(/^\uFEFF/, '');
    const parsed = parsePlateImport(text);
    const productTagCodes = await this.getProductTagCodeSets();
    const seenCodes = new Set<string>();
    let created = 0;
    let updated = 0;
    let skipped = parsed.errors.length;

    for (const row of parsed.rows) {
      if (seenCodes.has(row.plateCode)) {
        skipped += 1;
        parsed.errors.push(`Duplicate PLATE CODE in file: ${row.plateCode}`);
        continue;
      }

      seenCodes.add(row.plateCode);
      const productTagErrors = this.collectProductTagErrors(
        row,
        productTagCodes,
        `PLATE CODE ${row.plateCode}`,
      );

      if (productTagErrors.length) {
        skipped += 1;
        parsed.errors.push(...productTagErrors);
        continue;
      }

      const existing = await this.prisma.plateMaster.findUnique({
        where: { plateCode: row.plateCode },
      });

      await this.prisma.plateMaster.upsert({
        where: { plateCode: row.plateCode },
        create: row,
        update: row,
      });

      if (existing) {
        updated += 1;
      } else {
        created += 1;
      }
    }

    return {
      totalRows: parsed.rows.length,
      created,
      updated,
      skipped,
      errors: parsed.errors.slice(0, 25),
    };
  }

  async exportCsv(query: PlateQuery) {
    const rows = await this.prisma.plateMaster.findMany({
      where: this.buildWhere(query),
      orderBy: { plateCode: 'asc' },
    });

    return [
      plateCsvHeaders.join(','),
      ...rows.map((row) =>
        [
          row.sourceId ?? '',
          row.holderCode,
          row.plateName,
          row.brakeName,
          row.plateCode,
          row.brakeType,
          row.brakeTypeCode,
          row.loadCarryingType,
          row.loadCarryingCode,
          row.mountingType,
          row.mountingCode,
          row.fittingSize,
          row.fittingSizeCode,
          row.bearingType,
          row.bearingCode,
          row.fittingType,
          row.fittingCode,
          row.yokeName ?? '',
          row.yokeCode ?? '',
          row.status,
        ].map(csvCell).join(','),
      ),
    ].join('\n');
  }

  private async ensureExists(id: string) {
    const existing = await this.prisma.plateMaster.findUnique({ where: { id } });

    if (!existing) {
      throw new NotFoundException('Plate record not found');
    }
  }

  private buildWhere(query: PlateQuery): Prisma.PlateMasterWhereInput {
    const and: Prisma.PlateMasterWhereInput[] = [];
    const search = clean(query.search);

    if (search) {
      and.push({
        OR: [
          { holderCode: { contains: search, mode: 'insensitive' } },
          { plateName: { contains: search, mode: 'insensitive' } },
          { brakeName: { contains: search, mode: 'insensitive' } },
          { plateCode: { contains: search, mode: 'insensitive' } },
          { brakeType: { contains: search, mode: 'insensitive' } },
          { loadCarryingType: { contains: search, mode: 'insensitive' } },
          { mountingType: { contains: search, mode: 'insensitive' } },
          { fittingSize: { contains: search, mode: 'insensitive' } },
          { bearingType: { contains: search, mode: 'insensitive' } },
          { fittingType: { contains: search, mode: 'insensitive' } },
          { yokeName: { contains: search, mode: 'insensitive' } },
          { yokeCode: { contains: search, mode: 'insensitive' } },
          { status: { contains: search, mode: 'insensitive' } },
        ],
      });
    }

    if (clean(query.status)) {
      and.push({ status: normalizeStatus(query.status) });
    }

    if (clean(query.holderCode)) {
      and.push({ holderCode: clean(query.holderCode).toUpperCase() });
    }

    if (clean(query.brakeName)) {
      and.push({ brakeName: clean(query.brakeName) });
    }

    return and.length ? { AND: and } : {};
  }

  private async getStatusOptions() {
    const items = await this.prisma.plateMaster.findMany({
      distinct: ['status'],
      orderBy: { status: 'asc' },
      select: { status: true },
    });

    return {
      statuses: items.map((item) => item.status),
    };
  }

  private async getProductTagCodeSets() {
    const [
      brakingSystems,
      loadCarryCapacities,
      mountingTypes,
      headerTypes,
      bearingInFrameTypes,
      fittingTypes,
    ] = await Promise.all([
      this.prisma.brakingSystem.findMany({ select: { code: true } }),
      this.prisma.loadCarryCapacity.findMany({ select: { code: true } }),
      this.prisma.mountingType.findMany({ select: { code: true } }),
      this.prisma.headerType.findMany({ select: { code: true } }),
      this.prisma.bearingInFrameType.findMany({ select: { code: true } }),
      this.prisma.fittingSize.findMany({ select: { code: true } }),
    ]);

    return {
      brakingSystem: new Set(brakingSystems.map((row) => row.code)),
      loadCarryingCapacity: new Set(loadCarryCapacities.map((row) => row.code)),
      mountingType: new Set(mountingTypes.map((row) => row.code)),
      headerType: new Set(headerTypes.map((row) => row.code)),
      bearingInFrame: new Set(bearingInFrameTypes.map((row) => row.code)),
      fittingType: new Set(fittingTypes.map((row) => row.code)),
    };
  }

  private collectProductTagErrors(
    row: Pick<
      PlateImportRow,
      | 'plateCode'
      | 'brakeTypeCode'
      | 'loadCarryingCode'
      | 'mountingCode'
      | 'fittingSizeCode'
      | 'bearingCode'
      | 'fittingCode'
    >,
    codeSets: {
      brakingSystem: Set<string>;
      loadCarryingCapacity: Set<string>;
      mountingType: Set<string>;
      headerType: Set<string>;
      bearingInFrame: Set<string>;
      fittingType: Set<string>;
    },
    source: string,
  ) {
    const errors: string[] = [];

    this.appendMissingProductTagError(
      errors,
      'Braking System',
      row.brakeTypeCode,
      codeSets.brakingSystem,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Load Carrying Capacity',
      row.loadCarryingCode,
      codeSets.loadCarryingCapacity,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Mounting Type',
      row.mountingCode,
      codeSets.mountingType,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Header Type',
      row.fittingSizeCode,
      codeSets.headerType,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Bearing In Frame',
      row.bearingCode,
      codeSets.bearingInFrame,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Fitting Type',
      row.fittingCode,
      codeSets.fittingType,
      source,
    );

    return errors;
  }

  private async ensureProductTagCodes(
    row: Pick<
      PlateImportRow,
      | 'plateCode'
      | 'brakeTypeCode'
      | 'loadCarryingCode'
      | 'mountingCode'
      | 'fittingSizeCode'
      | 'bearingCode'
      | 'fittingCode'
    >,
    source: string,
  ) {
    const errors = this.collectProductTagErrors(
      row,
      await this.getProductTagCodeSets(),
      source,
    );

    if (errors.length) {
      throw new BadRequestException(errors);
    }
  }

  private appendMissingProductTagError(
    errors: string[],
    label: string,
    value: string,
    codes: Set<string>,
    source: string,
  ) {
    const code = clean(value).toUpperCase();

    if (!code) {
      return;
    }

    if (!codes.has(code)) {
      errors.push(`${label} code "${code}" from ${source} was not found in Product Tags.`);
    }
  }

  private validateWrite(dto: PlateWriteDto): Prisma.PlateMasterCreateInput {
    const data: PlateImportRow = {
      sourceId: parseOptionalInteger(dto.sourceId),
      holderCode: clean(dto.holderCode).toUpperCase(),
      plateName: clean(dto.plateName),
      brakeName: clean(dto.brakeName),
      plateCode: clean(dto.plateCode).toUpperCase(),
      brakeType: clean(dto.brakeType),
      brakeTypeCode: clean(dto.brakeTypeCode).toUpperCase(),
      loadCarryingType: clean(dto.loadCarryingType),
      loadCarryingCode: clean(dto.loadCarryingCode).toUpperCase(),
      mountingType: clean(dto.mountingType),
      mountingCode: clean(dto.mountingCode).toUpperCase(),
      fittingSize: clean(dto.fittingSize),
      fittingSizeCode: clean(dto.fittingSizeCode).toUpperCase(),
      bearingType: clean(dto.bearingType),
      bearingCode: clean(dto.bearingCode).toUpperCase(),
      fittingType: clean(dto.fittingType),
      fittingCode: clean(dto.fittingCode).toUpperCase(),
      yokeName: clean(dto.yokeName) || undefined,
      yokeCode: clean(dto.yokeCode) || undefined,
      status: normalizeStatus(dto.status),
    };

    const requiredFields: Array<[string, string]> = [
      [data.holderCode, 'Holder code is required.'],
      [data.plateName, 'Plate name is required.'],
      [data.brakeName, 'Brake name is required.'],
      [data.plateCode, 'Plate code is required.'],
      [data.brakeType, 'Brake type is required.'],
      [data.mountingType, 'Mounting type is required.'],
      [data.fittingSize, 'Fitting size is required.'],
      [data.bearingType, 'Bearing type is required.'],
      [data.fittingType, 'Fitting type is required.'],
    ];

    const errors = requiredFields
      .filter(([value]) => !value)
      .map(([, message]) => message);

    if (errors.length) {
      throw new BadRequestException(errors);
    }

    return data;
  }

  private toPositiveNumber(value: string | undefined, fallback: number) {
    const parsed = Number.parseInt(value || '', 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
  }
}

function parseOptionalInteger(value: PlateWriteDto['sourceId']) {
  if (value === null || value === undefined || value === '') {
    return undefined;
  }

  const parsed = Number.parseInt(String(value), 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function csvCell(value: string | number) {
  const text = String(value);

  if (/[",\n\r]/.test(text)) {
    return `"${text.replace(/"/g, '""')}"`;
  }

  return text;
}
