import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma, HolderMaster } from '../generated/prisma/index';
import { PrismaService } from '../prisma/prisma.service';
import {
  HolderImportRow,
  clean,
  holderCsvHeaders,
  normalizeHolderInch,
  normalizeStatus,
  parseInchValue,
  parseHolderImport,
} from './holder-master-import';

type HolderQuery = {
  page?: string;
  pageSize?: string;
  search?: string;
  status?: string;
  holderInch?: string;
  bodyCode?: string;
  sortBy?: string;
  sortOrder?: string;
};

type HolderWriteDto = {
  sourceId?: number | string | null;
  holderInch?: string;
  holderName?: string;
  holderCode?: string;
  bodyName?: string;
  bodyCode?: string;
  yorkeName?: string | null;
  yorkeCode?: string | null;
  yorkeLoad?: number | string | null;
  status?: string;
};

const sortColumns = new Set([
  'holderCode',
  'holderInch',
  'holderInchValue',
  'holderName',
  'bodyName',
  'bodyCode',
  'yorkeLoad',
  'status',
  'updatedAt',
]);

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

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

    const [total, items, filterOptions] = await Promise.all([
      this.prisma.holderMaster.count({ where }),
      this.prisma.holderMaster.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,
    };
  }

  async getInchOptions() {
    const items = await this.prisma.holderMaster.findMany({
      distinct: ['holderInchValue'],
      where: { holderInchValue: { gt: 0 } },
      orderBy: { holderInchValue: 'asc' },
      select: { holderInchValue: true },
    });

    return items.map((item) => ({
      value: item.holderInchValue,
      label: `${item.holderInchValue} INCH`,
    }));
  }

  async getBodyCodeOptions() {
    const bodyCodes = await this.prisma.holderMaster.findMany({
      distinct: ['bodyCode'],
      orderBy: { bodyCode: 'asc' },
      select: { bodyCode: true },
    });

    return bodyCodes.map((item) => item.bodyCode);
  }

  async create(dto: HolderWriteDto) {
    const data = this.validateWrite(dto);
    await this.ensureBodyCodeExists(data.bodyCode, `Holder code "${data.holderCode}"`);

    return this.prisma.holderMaster.create({ data });
  }

  async update(id: string, dto: HolderWriteDto) {
    await this.ensureExists(id);
    const data = this.validateWrite(dto);
    await this.ensureBodyCodeExists(data.bodyCode, `Holder code "${data.holderCode}"`);

    return this.prisma.holderMaster.update({
      where: { id },
      data,
    });
  }

  async delete(id: string) {
    await this.ensureExists(id);
    await this.prisma.holderMaster.delete({ where: { id } });

    return { deleted: true };
  }

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

      seenCodes.add(row.holderCode);

      if (row.bodyCode && !bodyTypeCodes.has(row.bodyCode)) {
        skipped += 1;
        parsed.errors.push(
          `Body Type code "${row.bodyCode}" for HOLDER CODE ${row.holderCode} was not found in Product Tags.`,
        );
        continue;
      }

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

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

    return [
      holderCsvHeaders.join(','),
      ...rows.map((row) =>
        [
          row.sourceId ?? '',
          row.holderInch,
          row.holderName,
          row.holderCode,
          row.bodyName,
          row.bodyCode,
          row.yorkeName ?? '',
          row.yorkeCode ?? '',
          row.yorkeLoad ?? '',
          row.status,
        ]
          .map(csvCell)
          .join(','),
      ),
    ].join('\n');
  }

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

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

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

    if (search) {
      and.push({
        OR: [
          { holderCode: { contains: search, mode: 'insensitive' } },
          { holderInch: { contains: search, mode: 'insensitive' } },
          { holderName: { contains: search, mode: 'insensitive' } },
          { bodyName: { contains: search, mode: 'insensitive' } },
          { bodyCode: { contains: search, mode: 'insensitive' } },
          { yorkeName: { contains: search, mode: 'insensitive' } },
          { yorkeCode: { contains: search, mode: 'insensitive' } },
          { status: { contains: search, mode: 'insensitive' } },
        ],
      });
    }

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

    if (clean(query.holderInch)) {
      const inchValue = parseInchValue(query.holderInch);

      if (inchValue) {
        and.push({ holderInchValue: inchValue });
      }
    }

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

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

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

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

  private async getBodyTypeCodes() {
    const rows = await this.prisma.bodyType.findMany({
      select: { code: true },
    });

    return new Set(rows.map((row) => row.code));
  }

  private async ensureBodyCodeExists(bodyCode: string, source: string) {
    const code = clean(bodyCode).toUpperCase();

    if (!code) {
      return;
    }

    const existing = await this.prisma.bodyType.findUnique({
      where: { code },
      select: { id: true },
    });

    if (!existing) {
      throw new BadRequestException(
        `Body Type code "${code}" from ${source} was not found in Product Tags.`,
      );
    }
  }

  private validateWrite(dto: HolderWriteDto): Prisma.HolderMasterCreateInput {
    const holderInchValue = parseInchValue(dto.holderInch);
    const data: HolderImportRow = {
      sourceId: parseOptionalInteger(dto.sourceId),
      holderInchValue: holderInchValue || 0,
      holderInch: normalizeHolderInch(dto.holderInch),
      holderName: clean(dto.holderName),
      holderCode: clean(dto.holderCode).toUpperCase(),
      bodyName: clean(dto.bodyName),
      bodyCode: clean(dto.bodyCode).toUpperCase(),
      yorkeName: clean(dto.yorkeName) || undefined,
      yorkeCode: clean(dto.yorkeCode) || undefined,
      yorkeLoad: parseOptionalNumber(dto.yorkeLoad),
      status: normalizeStatus(dto.status),
    };

    const errors: string[] = [];

    if (!data.holderCode) {
      errors.push('Holder code is required.');
    }

    if (!data.holderInch || !holderInchValue) {
      errors.push('Holder inch must be between 1 and 15.');
    }

    if (!data.holderName) {
      errors.push('Holder name is required.');
    }

    if (!data.bodyName) {
      errors.push('Body name is required.');
    }

    if (!data.bodyCode) {
      errors.push('Body code is required.');
    }

    if (data.yorkeLoad !== undefined && data.yorkeLoad < 0) {
      errors.push('Yorke load cannot be negative.');
    }

    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: HolderWriteDto['sourceId']) {
  if (value === null || value === undefined || value === '') {
    return undefined;
  }

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

function parseOptionalNumber(value: HolderWriteDto['yorkeLoad']) {
  if (value === null || value === undefined || value === '') {
    return undefined;
  }

  const parsed = Number.parseFloat(String(value));
  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;
}
