import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { CostingProcess, Prisma } from '../generated/prisma/index';
import { exportRows, normalizeImportRow, parseImportRows } from '../common/tabular-file.util';
import { PrismaService } from '../prisma/prisma.service';
import {
  ImportCommitDto,
  ImportPreviewDto,
  ProcessCoatingInput,
  ProcessMachineInput,
  ProcessMaterialInput,
  ProcessQuery,
  ProcessWriteDto,
} from './process-costing.dto';

const processTypes = ['Holder Process', 'Wheel Process'];
const sortColumns = new Set(['processCode', 'processName', 'processType', 'materialCost', 'machineCost', 'totalCost', 'updatedAt']);
const steelDensity = 7.8;
type MaterialWithCategory = Prisma.RawMaterialGetPayload<{ include: { category: true } }>;

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

  async list(query: ProcessQuery) {
    const page = positiveInt(query.page, 1);
    const pageSize = Math.min(positiveInt(query.pageSize, 10), 100);
    const where = this.buildWhere(query);
    const sortBy = sortColumns.has(query.sortBy || '') ? (query.sortBy as keyof CostingProcess) : 'processCode';
    const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
    const [total, items] = await Promise.all([
      this.prisma.costingProcess.count({ where }),
      this.prisma.costingProcess.findMany({
        where,
        orderBy: { [sortBy]: sortOrder } as Prisma.CostingProcessOrderByWithRelationInput,
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
    ]);
    return { items, meta: { total, page, pageSize, totalPages: Math.max(1, Math.ceil(total / pageSize)) }, filterOptions: await this.options() };
  }

  async detail(id: string) {
    const item = await this.prisma.costingProcess.findUnique({ where: { id } });
    if (!item) throw new NotFoundException('Process costing not found');
    return item;
  }

  async create(dto: ProcessWriteDto) {
    const data = (await this.validate(dto)) as Prisma.CostingProcessCreateInput;
    return this.prisma.costingProcess.create({ data });
  }

  async update(id: string, dto: ProcessWriteDto) {
    await this.detail(id);
    const data = (await this.validate(dto)) as Prisma.CostingProcessUpdateInput;
    return this.prisma.costingProcess.update({ where: { id }, data });
  }

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

  async options() {
    const [holders, wheels, materials, machines, settings] = await Promise.all([
      this.prisma.holderMaster.findMany({ orderBy: { holderName: 'asc' }, take: 300 }),
      this.prisma.wheelMaster.findMany({ orderBy: { wheelName: 'asc' }, take: 300 }),
      this.prisma.rawMaterial.findMany({ include: { category: true, uom: true }, orderBy: { materialName: 'asc' }, take: 500 }),
      this.prisma.machinery.findMany({ orderBy: { machineryName: 'asc' }, take: 300 }),
      this.prisma.rawMaterialSettings.upsert({
        where: { key: 'raw_material' },
        update: {},
        create: { key: 'raw_material', processCodeAutoGenerate: true, processCodePrefix: 'PRC' },
      }),
    ]);

    return {
      processTypes,
      statuses: ['Active', 'Inactive', 'Draft'],
      inches: [
        ...new Set([
          ...holders.map((holder) => holder.holderInch),
          ...wheels.map((wheel) => `${wheel.wheelInchValue} INCH`),
        ].filter(Boolean)),
      ].sort(),
      holders,
      wheels,
      materials: materials.map((material) => ({
        ...material,
        categoryName: material.category?.categoryName || '',
        uomCode: material.uom?.uomCode || '',
      })),
      machines,
      settings,
      coatingTypes: ['EG coating', 'CED coating', 'Powder coating'],
    };
  }

  async calculate(dto: ProcessWriteDto) {
    const material = await this.calculateMaterial(dto.materialInput || {});
    const machine = this.calculateMachine(dto.machineInput || {});
    const coating = this.calculateCoating(dto.coatingInput || {}, material.weightKg, material.total);
    return {
      material,
      machine,
      coating,
      summary: {
        materialCost: round(material.total),
        machineCost: round(machine.total),
        coatingCost: round(coating.total),
        totalCost: round(material.total + machine.total + coating.total),
      },
    };
  }

  async export(query: ProcessQuery & { format?: string }) {
    const response = await this.list({ ...query, page: '1', pageSize: '10000' });
    return exportRows(
      'process',
      response.items.map((item) => ({
        Code: item.processCode,
        Name: item.processName,
        Type: item.processType,
        Inch: item.inch || '',
        'Material Cost': item.materialCost,
        'Machine Cost': item.machineCost,
        'Coating Cost': item.coatingCost,
        'Total Cost': item.totalCost,
        Status: item.status,
      })),
      query.format || 'xlsx',
    );
  }

  sample(format = 'xlsx') {
    return exportRows(
      'process_sample',
      [
        {
          Code: 'PRC-0001',
          Name: '2 INCH BASE PLATE EG',
          Type: 'Holder Process',
          Inch: '2 INCH',
          'Material Cost': 7.26,
          'Machine Cost': 1.61,
          'Coating Cost': 0.35,
          'Total Cost': 9.22,
          Status: 'Active',
        },
      ],
      format,
    );
  }

  previewImport(dto: ImportPreviewDto) {
    const rows = parseImportRows(dto.fileName || '', dto.content || '').map((row, index) => {
      const data = mapImportRow(row);
      const errors = validateImport(data);
      return { rowNumber: index + 1, data, valid: errors.length === 0, errors };
    });
    return { fileName: dto.fileName || '', rows, total: rows.length, valid: rows.filter((row) => row.valid).length, invalid: rows.filter((row) => !row.valid).length };
  }

  async commitImport(dto: ImportCommitDto) {
    let created = 0;
    const errors: string[] = [];
    for (const [index, raw] of (dto.rows || []).entries()) {
      try {
        const row = mapImportRow(raw as Record<string, string>);
        await this.create({
          processCode: row.processCode,
          processName: row.processName,
          processType: row.processType,
          inch: row.inch,
          materialCost: row.materialCost,
          machineCost: row.machineCost,
          coatingCost: row.coatingCost,
          totalCost: row.totalCost,
          status: row.status,
        });
        created += 1;
      } catch (error) {
        errors.push(`Row ${index + 1}: ${extractError(error)}`);
      }
    }
    return { created, failed: errors.length, errors };
  }

  private async validate(dto: ProcessWriteDto): Promise<Prisma.CostingProcessCreateInput> {
    const settings = await this.prisma.rawMaterialSettings.upsert({
      where: { key: 'raw_material' },
      update: {},
      create: { key: 'raw_material', processCodeAutoGenerate: true, processCodePrefix: 'PRC' },
    });
    const calculated = await this.calculate(dto);
    const materialCost = parseNumber(dto.materialCost, calculated.summary.materialCost);
    const machineCost = parseNumber(dto.machineCost, calculated.summary.machineCost);
    const coatingCost = parseNumber(dto.coatingCost, calculated.summary.coatingCost);
    const totalCost = parseNumber(dto.totalCost, materialCost + machineCost + coatingCost);
    const processType = processTypes.includes(clean(dto.processType)) ? clean(dto.processType) : 'Holder Process';
    const processCode = settings.processCodeAutoGenerate && !clean(dto.processCode)
      ? await this.nextProcessCode(settings.processCodePrefix || 'PRC')
      : clean(dto.processCode).toUpperCase();
    const errors: string[] = [];
    if (!processCode) errors.push('Process code is required.');
    if (!clean(dto.processName)) errors.push('Process name is required.');
    if (!processType) errors.push('Process type is required.');
    if (errors.length) throw new BadRequestException(errors);
    return {
      processCode,
      processName: clean(dto.processName),
      processType,
      inch: clean(dto.inch) || null,
      selectedItems: (dto.selectedItems || []) as Prisma.InputJsonValue,
      materialInput: (dto.materialInput || {}) as Prisma.InputJsonValue,
      machineInput: (dto.machineInput || {}) as Prisma.InputJsonValue,
      coatingInput: (dto.coatingInput || {}) as Prisma.InputJsonValue,
      materialCost: round(materialCost),
      machineCost: round(machineCost),
      coatingCost: round(coatingCost),
      totalCost: round(totalCost),
      status: normalizeStatus(dto.status, 'Active'),
      createdBy: clean(dto.createdBy) || 'Starline Admin',
    };
  }

  private async nextProcessCode(prefix: string) {
    const count = await this.prisma.costingProcess.count();
    return `${prefix}-${String(count + 1).padStart(4, '0')}`;
  }

  private async calculateMaterial(input: ProcessMaterialInput) {
    let material: MaterialWithCategory | null = null;
    if (clean(input.materialId)) {
      material = await this.prisma.rawMaterial.findUnique({
        where: { id: clean(input.materialId) },
        include: { category: true },
      });
    }
    const category = clean(input.materialCategory || material?.category?.categoryName).toUpperCase();
    const unitPrice = parseNumber(input.unitPrice, material?.costPerUnit || 0);
    const length = parseNumber(input.length, 0);
    const width = parseNumber(input.width, 0);
    const thickness = parseNumber(input.thickness, 0);
    const quantity = parseNumber(input.quantity, 1);
    const weightGram = parseNumber(input.weightGram, 0);
    const dia = parseNumber(input.dia, 0);
    const innerDia = parseNumber(input.innerDia, 0);
    const outerDia = parseNumber(input.outerDia, 0);
    const noOfSetsPerSheet = parseNumber(input.noOfSetsPerSheet, 1) || 1;
    let total = 0;
    let weightKg = weightGram / 1000;
    const notes: string[] = [];

    if (category.includes('SHEET')) {
      weightKg = (length * width * thickness * steelDensity) / 1000000;
      const oneSheetRate = weightKg * unitPrice;
      total = oneSheetRate / noOfSetsPerSheet;
      notes.push('Sheet total = one sheet rate / no of sets per sheet');
    } else if (category.includes('BRAKE')) {
      weightKg = (length * width * thickness * steelDensity) / 1000000;
      total = weightKg * unitPrice;
      notes.push('Brake total = L x W x T x density x unit price');
    } else if (category.includes('SLEEVE')) {
      weightKg = ((Math.PI / 4) * (outerDia ** 2 - innerDia ** 2) * length * steelDensity) / 1000000;
      total = weightKg * unitPrice;
      notes.push('Sleeve total = outer area - inner area x length x density x unit price');
    } else if (category.includes('ROD')) {
      weightKg = ((Math.PI / 4) * dia ** 2 * length * steelDensity) / 1000000;
      total = weightKg * unitPrice;
      notes.push('Rod total = dia area x length x density x unit price');
    } else if (category.includes('GREASE')) {
      total = (unitPrice / 1000) * weightGram;
      notes.push('Grease total = unit price / 1000 x weight gram');
    } else if (category.includes('WHEEL')) {
      total = unitPrice * weightGram;
      notes.push('Wheel total = unit price x weight');
    } else {
      total = unitPrice * quantity;
      notes.push('Bearing/raw material total = unit price x quantity');
    }

    return {
      materialId: material?.id || clean(input.materialId),
      materialName: material?.materialName || clean(input.materialName),
      category,
      unitPrice,
      weightKg: round(weightKg),
      quantity,
      total: round(total),
      notes,
    };
  }

  private calculateMachine(input: ProcessMachineInput) {
    const running = parseNumber(input.runningCostPerHour, 0);
    const labour = parseNumber(input.labourCostPerHour, 0);
    const production = parseNumber(input.productionPerHour, 1) || 1;
    const quantity = parseNumber(input.quantity, 1);
    const productionCostPerPiece = (running + labour) / production;
    const cutting = parseNumber(input.oneStripCuttingCharge, 0) + parseNumber(input.onePieceCuttingCharge, 0) * quantity;
    const galvanizing = clean(input.galvanizingMode).toLowerCase() === 'per kg'
      ? parseNumber(input.galvanizingCharge, 0)
      : parseNumber(input.galvanizingCharge, 0) * quantity;
    const total = productionCostPerPiece * quantity + cutting + galvanizing;
    return {
      processName: clean(input.processName),
      machineId: clean(input.machineId),
      productionCostPerPiece: round(productionCostPerPiece),
      cuttingCost: round(cutting),
      galvanizingCost: round(galvanizing),
      total: round(total),
    };
  }

  private calculateCoating(input: ProcessCoatingInput, materialWeightKg: number, materialTotal: number) {
    if (!input.enabled) return { coatingType: clean(input.coatingType), total: 0, basis: 'Disabled' };
    const price = parseNumber(input.price, 0);
    const quantity = parseNumber(input.quantity, 1);
    const weightKg = parseNumber(input.weightKg, materialWeightKg) || parseNumber(input.weightGram, 0) / 1000;
    const isPerKg = clean(input.mode).toLowerCase() !== 'per pcs';
    const total = isPerKg ? price * weightKg : price * quantity;
    return {
      coatingType: clean(input.coatingType) || 'EG coating',
      mode: isPerKg ? 'Per KG' : 'Per PCS',
      weightKg: round(weightKg),
      materialTotal: round(materialTotal),
      total: round(total),
    };
  }

  private buildWhere(query: ProcessQuery): Prisma.CostingProcessWhereInput {
    const and: Prisma.CostingProcessWhereInput[] = [];
    const search = clean(query.search);
    if (search) {
      and.push({
        OR: [
          { processCode: { contains: search, mode: 'insensitive' } },
          { processName: { contains: search, mode: 'insensitive' } },
        ],
      });
    }
    if (clean(query.processType)) and.push({ processType: clean(query.processType) });
    if (clean(query.inch)) and.push({ inch: clean(query.inch) });
    if (clean(query.status)) and.push({ status: normalizeStatus(query.status, 'Active') });
    return and.length ? { AND: and } : {};
  }
}

function mapImportRow(row: Record<string, string>) {
  const data = normalizeImportRow(row);
  return {
    processCode: data.code || data.processCode,
    processName: data.name || data.processName,
    processType: data.type || data.processType || 'Holder Process',
    inch: data.inch,
    materialCost: data.materialCost,
    machineCost: data.machineCost,
    coatingCost: data.coatingCost,
    totalCost: data.totalCost,
    status: data.status || 'Active',
  };
}

function validateImport(row: Record<string, unknown>) {
  const errors: string[] = [];
  if (!clean(row.processName)) errors.push('Process name is required.');
  if (!processTypes.includes(clean(row.processType))) errors.push('Process type must be Holder Process or Wheel Process.');
  return errors;
}

function clean(value: unknown) {
  return String(value ?? '').trim();
}

function positiveInt(value: unknown, fallback: number) {
  const parsed = Number.parseInt(clean(value), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function parseNumber(value: unknown, fallback: number) {
  if (value === null || value === undefined || value === '') return fallback;
  const parsed = Number.parseFloat(String(value));
  return Number.isFinite(parsed) ? parsed : fallback;
}

function normalizeStatus(value: unknown, fallback: string) {
  const status = clean(value);
  if (status.toLowerCase() === 'inactive') return 'Inactive';
  if (status.toLowerCase() === 'draft') return 'Draft';
  return fallback;
}

function round(value: number) {
  return Math.round((Number.isFinite(value) ? value : 0) * 100) / 100;
}

function extractError(error: unknown) {
  if (error instanceof BadRequestException) {
    const response = error.getResponse() as any;
    return Array.isArray(response.message) ? response.message.join(', ') : response.message || error.message;
  }
  if (error instanceof Error) return error.message;
  return 'Import failed.';
}
