import type { Site, CoveragePoint, CoverageResult, CoverageSettings, GridPoint } from '@/types/index.ts';
import { logger } from '@/utils/logger.ts';

export class RFCalculator {
  /**
   * Calculate coverage for multiple sites using Web Workers.
   */
  async calculateCoverage(
    sites: Site[],
    mapBounds: { north: number; south: number; east: number; west: number },
    settings: CoverageSettings
  ): Promise<CoverageResult> {
    const startTime = performance.now();

    // Generate grid of points
    const gridPoints = this.generateGrid(mapBounds, settings.resolution);

    // Determine number of workers
    const numWorkers = Math.min(4, navigator.hardwareConcurrency || 4);
    const chunks = this.chunkArray(gridPoints, numWorkers);

    // Serialize site data for workers
    const sitesData = sites.map((s) => ({
      id: s.id,
      lat: s.lat,
      lon: s.lon,
      height: s.height,
      power: s.power,
      gain: s.gain,
      frequency: s.frequency,
      antennaType: s.antennaType,
      azimuth: s.azimuth,
      beamwidth: s.beamwidth,
    }));

    // Calculate in parallel using Web Workers
    const workers: Worker[] = [];
    const promises: Promise<CoveragePoint[]>[] = [];

    for (let i = 0; i < chunks.length; i++) {
      const worker = new Worker('/workers/rf-worker.js');
      workers.push(worker);
      promises.push(
        this.calculateChunk(worker, sitesData, chunks[i], settings.rsrpThreshold)
      );
    }

    const results = await Promise.all(promises);

    // Cleanup workers
    workers.forEach((w) => w.terminate());

    // Merge results (concat avoids stack overflow that .flat() can cause on huge arrays)
    const allPoints: CoveragePoint[] = [];
    for (const chunk of results) {
      for (const point of chunk) {
        allPoints.push(point);
      }
    }

    const calculationTime = performance.now() - startTime;

    return {
      points: allPoints,
      calculationTime,
      totalPoints: allPoints.length,
      settings,
    };
  }

  /**
   * Maximum grid points to prevent stack overflow / memory exhaustion.
   * 500k points at 4 workers = 125k per worker — safe for postMessage serialization.
   */
  private static readonly MAX_GRID_POINTS = 500_000;

  private generateGrid(
    bounds: { north: number; south: number; east: number; west: number },
    resolution: number
  ): GridPoint[] {
    // Convert resolution to degrees
    const latStep = resolution / 111000; // ~111km per degree latitude
    const centerLat = (bounds.north + bounds.south) / 2;
    const lonStep =
      resolution / (111000 * Math.cos((centerLat * Math.PI) / 180));

    // Pre-check total count to avoid building an array we'd throw away
    const latRange = bounds.north - bounds.south;
    const lonRange = bounds.east - bounds.west;
    const estimatedRows = Math.ceil(latRange / latStep) + 1;
    const estimatedCols = Math.ceil(lonRange / lonStep) + 1;
    const estimated = estimatedRows * estimatedCols;

    if (estimated > RFCalculator.MAX_GRID_POINTS) {
      // Auto-coarsen: pick a resolution that fits within the cap
      const autoResolution = Math.ceil(
        Math.sqrt((latRange * 111000 * lonRange * 111000 * Math.cos((centerLat * Math.PI) / 180)) /
          RFCalculator.MAX_GRID_POINTS)
      );
      const coarsenedLatStep = autoResolution / 111000;
      const coarsenedLonStep =
        autoResolution / (111000 * Math.cos((centerLat * Math.PI) / 180));

      logger.warn(
        `Grid too large (${estimated.toLocaleString()} points at ${resolution}m). ` +
        `Auto-coarsening to ~${autoResolution}m for safety (max ${RFCalculator.MAX_GRID_POINTS.toLocaleString()}).`
      );

      return this.buildGrid(bounds, coarsenedLatStep, coarsenedLonStep);
    }

    return this.buildGrid(bounds, latStep, lonStep);
  }

  private buildGrid(
    bounds: { north: number; south: number; east: number; west: number },
    latStep: number,
    lonStep: number
  ): GridPoint[] {
    const points: GridPoint[] = [];
    let lat = bounds.south;
    while (lat <= bounds.north) {
      let lon = bounds.west;
      while (lon <= bounds.east) {
        points.push({ lat, lon });
        lon += lonStep;
      }
      lat += latStep;
    }
    return points;
  }

  private chunkArray<T>(array: T[], numChunks: number): T[][] {
    const chunks: T[][] = [];
    const chunkSize = Math.ceil(array.length / numChunks);

    for (let i = 0; i < array.length; i += chunkSize) {
      chunks.push(array.slice(i, i + chunkSize));
    }

    return chunks;
  }

  private calculateChunk(
    worker: Worker,
    sites: unknown[],
    points: GridPoint[],
    rsrpThreshold: number
  ): Promise<CoveragePoint[]> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error('Worker timeout'));
      }, 30000);

      worker.postMessage({
        type: 'calculate',
        sites,
        points,
        rsrpThreshold,
      });

      worker.onmessage = (e: MessageEvent) => {
        clearTimeout(timeout);
        if (e.data.type === 'complete') {
          resolve(e.data.results);
        } else if (e.data.type === 'error') {
          reject(new Error(e.data.message));
        }
      };

      worker.onerror = (err) => {
        clearTimeout(timeout);
        reject(err);
      };
    });
  }
}
