import { Request, Response } from 'express';
import { BlockDataSource, Prisma } from '@prisma/client';
import { prisma } from '../models';
import {
  worldToGrid,
  krigingInterpolation,
  confidenceFromNearestDrill,
  type DrillPoint,
} from '../lib/module-b';

function parseCsvDrillPoints(buf: Buffer): DrillPoint[] {
  const text = buf.toString('utf8').trim();
  const lines = text.split(/\r?\n/).filter(Boolean);
  if (lines.length === 0) return [];
  let start = 0;
  const first = lines[0].toLowerCase();
  if (first.includes('x') && first.includes('y') && first.includes('z')) {
    start = 1;
  }
  const out: DrillPoint[] = [];
  for (let i = start; i < lines.length; i++) {
    const parts = lines[i].split(/[,;\t]/).map((s) => s.trim());
    if (parts.length < 4) continue;
    const x = Number(parts[0]);
    const y = Number(parts[1]);
    const z = Number(parts[2]);
    const ppm = Number(parts[3]);
    if ([x, y, z, ppm].some((n) => Number.isNaN(n))) continue;
    out.push({ x, y, z, ppm });
  }
  return out;
}

function tonsFromVolume(blockSizeM: number): number {
  const density = Number(process.env.MODULE_B_ROCK_TONNES_PER_M3 ?? '2.7');
  const vol = blockSizeM * blockSizeM * blockSizeM;
  return vol * density;
}

export const blockModelController = {
  async importCsv(req: Request, res: Response) {
    const file = (req as Request & { file?: { buffer: Buffer } }).file;
    if (!file?.buffer) {
      res.status(400).json({ success: false, error: 'CSV file required' });
      return;
    }
    const body = req.body as { license_id?: string; pit_id?: string; block_size_m?: string };
    const license_id = body.license_id;
    if (!license_id) {
      res.status(400).json({ success: false, error: 'license_id required' });
      return;
    }
    const lic = await prisma.license.findFirst({ where: { id: license_id, deleted_at: null } });
    if (!lic) {
      res.status(404).json({ success: false, error: 'License not found' });
      return;
    }
    const block_size_m = body.block_size_m ? Number(body.block_size_m) : 5;
    const pit_id = body.pit_id || undefined;
    if (pit_id) {
      const pit = await prisma.pit.findFirst({ where: { id: pit_id, license_id, deleted_at: null } });
      if (!pit) {
        res.status(404).json({ success: false, error: 'Pit not found' });
        return;
      }
    }

    const points = parseCsvDrillPoints(file.buffer);
    if (points.length === 0) {
      res.status(400).json({ success: false, error: 'No valid CSV rows (need X,Y,Z,PPM)' });
      return;
    }

    const tons = tonsFromVolume(block_size_m);
    let created = 0;
    for (const p of points) {
      const g = worldToGrid(p.x, p.y, p.z, block_size_m);
      await prisma.blockModel.upsert({
        where: {
          license_id_grid_i_grid_j_grid_k: {
            license_id,
            grid_i: g.grid_i,
            grid_j: g.grid_j,
            grid_k: g.grid_k,
          },
        },
        create: {
          license_id,
          pit_id,
          ...g,
          block_size_m,
          tons_estimate: tons,
          ppm_estimate: p.ppm,
          confidence: 100,
          data_source: BlockDataSource.DRILL_HOLE,
        },
        update: {
          pit_id,
          block_x: g.block_x,
          block_y: g.block_y,
          block_z: g.block_z,
          tons_estimate: tons,
          ppm_estimate: p.ppm,
          confidence: 100,
          data_source: BlockDataSource.DRILL_HOLE,
          last_updated: new Date(),
        },
      });
      created++;
    }
    res.json({ success: true, data: { imported: created, drill_points: points.length } });
  },

  async krigingInterpolate(req: Request, res: Response) {
    const b = req.body as {
      license_id?: string;
      pit_id?: string;
      block_size_m?: number;
      min_x?: number;
      max_x?: number;
      min_y?: number;
      max_y?: number;
      min_z?: number;
      max_z?: number;
    };
    if (!b.license_id) {
      res.status(400).json({ success: false, error: 'license_id required' });
      return;
    }
    const block_size_m = b.block_size_m ?? 5;
    const drillRows = await prisma.blockModel.findMany({
      where: {
        license_id: b.license_id,
        data_source: BlockDataSource.DRILL_HOLE,
      },
    });
    const points: DrillPoint[] = drillRows.map((r) => ({
      x: r.block_x,
      y: r.block_y,
      z: r.block_z,
      ppm: r.ppm_estimate,
    }));
    if (points.length === 0) {
      res.status(400).json({ success: false, error: 'No DRILL_HOLE blocks; import CSV first' });
      return;
    }

    const xs = points.map((p) => p.x);
    const ys = points.map((p) => p.y);
    const zs = points.map((p) => p.z);
    const pad = block_size_m * 2;
    const min_x = b.min_x ?? Math.min(...xs) - pad;
    const max_x = b.max_x ?? Math.max(...xs) + pad;
    const min_y = b.min_y ?? Math.min(...ys) - pad;
    const max_y = b.max_y ?? Math.max(...ys) + pad;
    const min_z = b.min_z ?? Math.min(...zs) - pad;
    const max_z = b.max_z ?? Math.max(...zs) + pad;

    const step = block_size_m;
    let upserted = 0;
    for (let x = min_x; x <= max_x; x += step) {
      for (let y = min_y; y <= max_y; y += step) {
        for (let z = min_z; z <= max_z; z += step) {
          const g = worldToGrid(x, y, z, block_size_m);
          const ppm = krigingInterpolation(points, g.block_x, g.block_y, g.block_z);
          const confidence = confidenceFromNearestDrill(
            points,
            g.block_x,
            g.block_y,
            g.block_z,
            block_size_m
          );
          const tons = tonsFromVolume(block_size_m);
          await prisma.blockModel.upsert({
            where: {
              license_id_grid_i_grid_j_grid_k: {
                license_id: b.license_id,
                grid_i: g.grid_i,
                grid_j: g.grid_j,
                grid_k: g.grid_k,
              },
            },
            create: {
              license_id: b.license_id,
              pit_id: b.pit_id,
              ...g,
              block_size_m,
              tons_estimate: tons,
              ppm_estimate: ppm,
              confidence,
              data_source: BlockDataSource.INTERPOLATION,
            },
            update: {
              pit_id: b.pit_id,
              block_x: g.block_x,
              block_y: g.block_y,
              block_z: g.block_z,
              tons_estimate: tons,
              ppm_estimate: ppm,
              confidence,
              data_source: BlockDataSource.INTERPOLATION,
              last_updated: new Date(),
            },
          });
          upserted++;
        }
      }
    }
    res.json({ success: true, data: { upserted } });
  },

  async query(req: Request, res: Response) {
    const license_id = req.query.license_id as string;
    const minPpm = req.query.minPpm ? Number(req.query.minPpm) : undefined;
    if (!license_id) {
      res.status(400).json({ success: false, error: 'license_id required' });
      return;
    }
    const where: Prisma.BlockModelWhereInput = { license_id };
    if (minPpm !== undefined && !Number.isNaN(minPpm)) {
      where.ppm_estimate = { gte: minPpm };
    }
    const blocks = await prisma.blockModel.findMany({
      where,
      orderBy: { ppm_estimate: 'desc' },
      take: 5000,
    });
    res.json({ success: true, data: { blocks, count: blocks.length } });
  },

  async heatmap(req: Request, res: Response) {
    const license_id = req.query.license_id as string;
    const zSlice = req.query.z != null ? Number(req.query.z) : null;
    if (!license_id) {
      res.status(400).json({ success: false, error: 'license_id required' });
      return;
    }
    const blocks = await prisma.blockModel.findMany({
      where: { license_id },
    });
    const filtered =
      zSlice != null && !Number.isNaN(zSlice)
        ? blocks.filter((b) => Math.abs(b.block_z - zSlice) <= (b.block_size_m || 5))
        : blocks;
    if (filtered.length === 0) {
      res.status(404).json({ success: false, error: 'No blocks for heatmap' });
      return;
    }
    const xs = filtered.map((b) => b.block_x);
    const ys = filtered.map((b) => b.block_y);
    const ppms = filtered.map((b) => b.ppm_estimate);
    const minX = Math.min(...xs);
    const maxX = Math.max(...xs);
    const minY = Math.min(...ys);
    const maxY = Math.max(...ys);
    const minP = Math.min(...ppms);
    const maxP = Math.max(...ppms);
    const w = 400;
    const h = 400;
    const pad = 20;
    const sx = (x: number) => pad + ((x - minX) / (maxX - minX || 1)) * (w - 2 * pad);
    const sy = (y: number) => h - pad - ((y - minY) / (maxY - minY || 1)) * (h - 2 * pad);
    const color = (ppm: number) => {
      const t = maxP > minP ? (ppm - minP) / (maxP - minP) : 0.5;
      const r = Math.round(255 * t);
      const b = Math.round(255 * (1 - t));
      return `rgb(${r},80,${b})`;
    };
    let circles = '';
    for (const b of filtered) {
      circles += `<circle cx="${sx(b.block_x)}" cy="${sy(b.block_y)}" r="4" fill="${color(
        b.ppm_estimate
      )}" opacity="0.85"><title>${b.ppm_estimate.toFixed(2)} ppm</title></circle>`;
    }
    const svg = `<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${circles}</svg>`;
    res.type('image/svg+xml').send(svg);
  },

  async reserves(req: Request, res: Response) {
    const license_id = req.query.license_id as string;
    if (!license_id) {
      res.status(400).json({ success: false, error: 'license_id required' });
      return;
    }
    const agg = await prisma.blockModel.aggregate({
      where: { license_id },
      _sum: { tons_estimate: true },
      _avg: { ppm_estimate: true },
      _count: true,
    });
    res.json({
      success: true,
      data: {
        total_tons: agg._sum.tons_estimate ?? 0,
        avg_ppm: agg._avg.ppm_estimate ?? 0,
        block_count: agg._count,
      },
    });
  },
};
