import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma, WheelMaster } from '../generated/prisma/index';
import { PrismaService } from '../prisma/prisma.service';
import { clean, normalizeStatus } from '../holder-master/holder-master-import';
import {
  parseWheelInchValue,
  parseWheelImport,
  wheelCsvHeaders,
  WheelImportRow,
} from './wheel-master-import';

type WheelQuery = {
  page?: string;
  pageSize?: string;
  search?: string;
  status?: string;
  wheelType?: string;
  bearingType?: string;
  sortBy?: string;
  sortOrder?: string;
};

type WheelWriteDto = {
  sourceId?: number | string | null;
  wheelName?: string;
  wheelCode?: string;
  wheelSize?: string;
  wheelSizeCode?: string;
  wheelType?: string;
  wheelTypeCode?: string;
  bearingType?: string;
  bearingCode?: string;
  dustCoverName?: string;
  dustCoverCode?: string;
  wheelColor?: string;
  wheelColorCode?: string;
  dynamicLoadCarryCapacity?: string;
  dynamicLoadCarryCapacityCode?: string;
  wheelTypeSubName?: string;
  wheelTypeSubNameCode?: string;
  status?: string;
};

const sortColumns = new Set([
  'wheelCode',
  'wheelInchValue',
  'wheelName',
  'wheelType',
  'bearingType',
  'wheelColor',
  'status',
  'updatedAt',
]);

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

  async list(query: WheelQuery) {
    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 WheelMaster)
      : 'wheelCode';
    const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
    const orderBy = {
      [sortBy]: sortOrder,
    } as Prisma.WheelMasterOrderByWithRelationInput;

    const [total, items, statuses] = await Promise.all([
      this.prisma.wheelMaster.count({ where }),
      this.prisma.wheelMaster.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 getWheelTypeOptions() {
    const items = await this.prisma.wheelMaster.findMany({
      distinct: ['wheelType'],
      orderBy: { wheelType: 'asc' },
      select: { wheelType: true },
    });

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

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

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

  async create(dto: WheelWriteDto) {
    const data = this.validateWrite(dto);
    await this.ensureProductTagCodes(data, `Wheel code "${data.wheelCode}"`);
    return this.prisma.wheelMaster.create({ data });
  }

  async update(id: string, dto: WheelWriteDto) {
    await this.ensureExists(id);
    const data = this.validateWrite(dto);
    await this.ensureProductTagCodes(data, `Wheel code "${data.wheelCode}"`);
    return this.prisma.wheelMaster.update({
      where: { id },
      data,
    });
  }

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

  async importFile(buffer: Buffer) {
    const text = buffer.toString('utf8').replace(/^\uFEFF/, '');
    const parsed = parseWheelImport(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.wheelCode)) {
        skipped += 1;
        parsed.errors.push(`Duplicate WHEEL CODE in file: ${row.wheelCode}`);
        continue;
      }

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

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

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

      await this.prisma.wheelMaster.upsert({
        where: { wheelCode: row.wheelCode },
        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: WheelQuery) {
    const rows = await this.prisma.wheelMaster.findMany({
      where: this.buildWhere(query),
      orderBy: { wheelCode: 'asc' },
    });

    return [
      wheelCsvHeaders.join(','),
      ...rows.map((row) =>
        [
          row.sourceId ?? '',
          row.wheelName,
          row.wheelCode,
          row.wheelSize,
          row.wheelSizeCode,
          row.wheelType,
          row.wheelTypeCode,
          row.bearingType,
          row.bearingCode,
          row.dustCoverName,
          row.dustCoverCode,
          row.wheelColor,
          row.wheelColorCode,
          row.dynamicLoadCarryCapacity,
          row.dynamicLoadCarryCapacityCode,
          row.wheelTypeSubName,
          row.wheelTypeSubNameCode,
          row.status,
        ].map(csvCell).join(','),
      ),
    ].join('\n');
  }

  private async ensureExists(id: string) {
    const existing = await this.prisma.wheelMaster.findUnique({ where: { id } });
    if (!existing) {
      throw new NotFoundException('Wheel record not found');
    }
  }

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

    if (search) {
      and.push({
        OR: [
          { wheelCode: { contains: search, mode: 'insensitive' } },
          { wheelName: { contains: search, mode: 'insensitive' } },
          { wheelSize: { contains: search, mode: 'insensitive' } },
          { wheelType: { contains: search, mode: 'insensitive' } },
          { bearingType: { contains: search, mode: 'insensitive' } },
          { dustCoverName: { contains: search, mode: 'insensitive' } },
          { wheelColor: { contains: search, mode: 'insensitive' } },
          { wheelTypeSubName: { contains: search, mode: 'insensitive' } },
          { status: { contains: search, mode: 'insensitive' } },
        ],
      });
    }

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

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

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

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

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

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

  private async getProductTagCodeSets() {
    const [
      wheelSizes,
      wheelTypes,
      bearingTypes,
      dustCoverTypes,
      wheelColors,
      loadCarryCapacities,
      wheelTypeTags,
    ] = await Promise.all([
      this.prisma.wheelSize.findMany({ select: { code: true } }),
      this.prisma.wheelType.findMany({ select: { code: true } }),
      this.prisma.bearingType.findMany({ select: { code: true } }),
      this.prisma.dustCoverType.findMany({ select: { code: true } }),
      this.prisma.wheelColor.findMany({ select: { code: true } }),
      this.prisma.loadCarryCapacity.findMany({ select: { code: true } }),
      this.prisma.wheelTypeTag.findMany({ select: { code: true } }),
    ]);

    return {
      wheelSize: new Set(wheelSizes.map((row) => row.code)),
      wheelType: new Set(wheelTypes.map((row) => row.code)),
      bearingType: new Set(bearingTypes.map((row) => row.code)),
      dustCoverType: new Set(dustCoverTypes.map((row) => row.code)),
      wheelColor: new Set(wheelColors.map((row) => row.code)),
      loadCarryingCapacity: new Set(loadCarryCapacities.map((row) => row.code)),
      wheelTypeSub: new Set(wheelTypeTags.map((row) => row.code)),
    };
  }

  private collectProductTagErrors(
    row: Pick<
      WheelImportRow,
      | 'wheelCode'
      | 'wheelSizeCode'
      | 'wheelTypeCode'
      | 'bearingCode'
      | 'dustCoverCode'
      | 'wheelColorCode'
      | 'dynamicLoadCarryCapacityCode'
      | 'wheelTypeSubNameCode'
    >,
    codeSets: {
      wheelSize: Set<string>;
      wheelType: Set<string>;
      bearingType: Set<string>;
      dustCoverType: Set<string>;
      wheelColor: Set<string>;
      loadCarryingCapacity: Set<string>;
      wheelTypeSub: Set<string>;
    },
    source: string,
  ) {
    const errors: string[] = [];

    this.appendMissingProductTagError(
      errors,
      'Wheel Size',
      row.wheelSizeCode,
      codeSets.wheelSize,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Wheel Type',
      row.wheelTypeCode,
      codeSets.wheelType,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Bearing Type',
      row.bearingCode,
      codeSets.bearingType,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Dust Cover On Wheel',
      row.dustCoverCode,
      codeSets.dustCoverType,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Wheel Color',
      row.wheelColorCode,
      codeSets.wheelColor,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Load Carrying Capacity',
      row.dynamicLoadCarryCapacityCode,
      codeSets.loadCarryingCapacity,
      source,
    );
    this.appendMissingProductTagError(
      errors,
      'Wheel Type Sub',
      row.wheelTypeSubNameCode,
      codeSets.wheelTypeSub,
      source,
    );

    return errors;
  }

  private async ensureProductTagCodes(
    row: Pick<
      WheelImportRow,
      | 'wheelCode'
      | 'wheelSizeCode'
      | 'wheelTypeCode'
      | 'bearingCode'
      | 'dustCoverCode'
      | 'wheelColorCode'
      | 'dynamicLoadCarryCapacityCode'
      | 'wheelTypeSubNameCode'
    >,
    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: WheelWriteDto): WheelImportRow {
    const data: WheelImportRow = {
      sourceId: parseOptionalInteger(dto.sourceId),
      wheelInchValue: parseWheelInchValue(dto.wheelSize),
      wheelName: clean(dto.wheelName),
      wheelCode: clean(dto.wheelCode).toUpperCase(),
      wheelSize: clean(dto.wheelSize),
      wheelSizeCode: clean(dto.wheelSizeCode).toUpperCase(),
      wheelType: clean(dto.wheelType),
      wheelTypeCode: clean(dto.wheelTypeCode).toUpperCase(),
      bearingType: clean(dto.bearingType),
      bearingCode: clean(dto.bearingCode).toUpperCase(),
      dustCoverName: clean(dto.dustCoverName),
      dustCoverCode: clean(dto.dustCoverCode).toUpperCase(),
      wheelColor: clean(dto.wheelColor),
      wheelColorCode: clean(dto.wheelColorCode).toUpperCase(),
      dynamicLoadCarryCapacity: clean(dto.dynamicLoadCarryCapacity),
      dynamicLoadCarryCapacityCode: clean(dto.dynamicLoadCarryCapacityCode).toUpperCase(),
      wheelTypeSubName: clean(dto.wheelTypeSubName),
      wheelTypeSubNameCode: clean(dto.wheelTypeSubNameCode).toUpperCase(),
      status: normalizeStatus(dto.status),
    };

    const requiredFields: Array<[string, string]> = [
      [data.wheelName, 'Wheel name is required.'],
      [data.wheelCode, 'Wheel code is required.'],
      [data.wheelSize, 'Wheel size is required.'],
      [data.wheelType, 'Wheel type is required.'],
      [data.bearingType, 'Bearing type is required.'],
      [data.dustCoverName, 'Dust cover name is required.'],
      [data.wheelColor, 'Wheel color 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: WheelWriteDto['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;
}
