import { Prisma } from '@prisma/client';
import * as fs from 'fs';
import * as path from 'path';
import { parse } from 'csv-parse';
import { parse as parseSync } from 'csv-parse/sync';
import proj4 from 'proj4';
import { prisma } from '../../../config/database';
import { redis } from '../../../config/redis';
import { logger } from '../../../utils/logger';
import { isPointInPolygon, parseGeoJsonPolygon } from '../../../utils/spatial';
import { validateFilePath } from '../../../utils/path-validator';
import { env } from '../../../config/env';

// Statistics Canada Lambert Conformal Conic (EPSG:3347) → WGS84
proj4.defs('EPSG:3347', '+proj=lcc +lat_1=49 +lat_2=77 +lat_0=63.390675 +lon_0=-91.86666666666666 +x_0=6200000 +y_0=3000000 +ellps=GRS80 +units=m +no_defs');

const PROVINCE_MAP: Record<string, { name: string; abbr: string }> = {
  '10': { name: 'Newfoundland and Labrador', abbr: 'NL' },
  '11': { name: 'Prince Edward Island', abbr: 'PE' },
  '12': { name: 'Nova Scotia', abbr: 'NS' },
  '13': { name: 'New Brunswick', abbr: 'NB' },
  '24': { name: 'Quebec', abbr: 'QC' },
  '35': { name: 'Ontario', abbr: 'ON' },
  '46': { name: 'Manitoba', abbr: 'MB' },
  '47': { name: 'Saskatchewan', abbr: 'SK' },
  '48': { name: 'Alberta', abbr: 'AB' },
  '59': { name: 'British Columbia', abbr: 'BC' },
  '60': { name: 'Yukon', abbr: 'YT' },
  '61': { name: 'Northwest Territories', abbr: 'NT' },
  '62': { name: 'Nunavut', abbr: 'NU' },
};

interface NarFileInfo {
  filename: string;
  fullPath: string;
  sizeBytes: number;
  sizeFormatted: string;
  provinceCode: string;
  provinceName: string;
  provinceAbbr: string;
  type: 'address' | 'location';
  partNumber?: number;
}

interface NarDataset {
  provinceCode: string;
  provinceName: string;
  provinceAbbr: string;
  addressFiles: NarFileInfo[];
  locationFiles: NarFileInfo[];
  totalAddressSize: number;
  totalLocationSize: number;
}

interface LocationLookup {
  lat: number;
  lng: number;
  fedDistrict?: string;
  fedCode?: string;
}

export interface ServerImportOptions {
  provinceCode: string;
  filterType: 'none' | 'city' | 'postalPrefix' | 'cut';
  filterCity?: string;
  filterPostalPrefix?: string;
  cutId?: string;
  residentialOnly: boolean;
  deduplicateRadius: number;
  batchSize: number;
}

export interface ServerImportResult {
  provinceCode: string;
  provinceName: string;
  totalRows: number;
  locationsCreated: number;
  addressesCreated: number;
  skippedDuplicate: number;
  skippedOutOfBounds: number;
  skippedNonResidential: number;
  skippedInvalid: number;
  errors: string[];
  durationMs: number;
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}

function roundCoord(val: number, decimals: number = 5): number {
  const factor = Math.pow(10, decimals);
  return Math.round(val * factor) / factor;
}

function getDataDir(): string {
  return env.NAR_DATA_DIR;
}

/** Scan the NAR data directory and find the most recent dataset */
function findNarDirectory(baseDir: string): string | null {
  if (!fs.existsSync(baseDir)) return null;
  // Look for subdirectories with YYYYMM pattern (e.g., 202512)
  const entries = fs.readdirSync(baseDir, { withFileTypes: true });
  const dateDirs = entries
    .filter((e) => e.isDirectory() && /^\d{6}$/.test(e.name))
    .map((e) => e.name)
    .sort()
    .reverse();

  if (dateDirs.length > 0) return path.join(baseDir, dateDirs[0]!);

  // Fallback: check if Addresses/ exists directly
  if (fs.existsSync(path.join(baseDir, 'Addresses'))) return baseDir;
  return null;
}

function parseFileInfo(filePath: string, filename: string): NarFileInfo | null {
  // Match patterns like Address_13.csv, Address_24_part_1.csv, Location_59_part_2.csv
  const match = filename.match(/^(Address|Location)_(\d+)(?:_part_(\d+))?\.csv$/i);
  if (!match) return null;

  const type = match[1]!.toLowerCase() === 'address' ? 'address' : 'location';
  const provinceCode = match[2]!;
  const partNumber = match[3] ? parseInt(match[3], 10) : undefined;
  const prov = PROVINCE_MAP[provinceCode];
  if (!prov) return null;

  const stats = fs.statSync(filePath);
  return {
    filename,
    fullPath: filePath,
    sizeBytes: stats.size,
    sizeFormatted: formatBytes(stats.size),
    provinceCode,
    provinceName: prov.name,
    provinceAbbr: prov.abbr,
    type,
    partNumber,
  };
}

const PROGRESS_TTL = 3600; // 1 hour
const PROGRESS_INTERVAL = 5000; // write every 5000 rows

export interface ImportProgress {
  status: 'loading-locations' | 'importing' | 'creating-records' | 'complete' | 'failed';
  totalRows: number;
  locationsCreated: number;
  addressesCreated: number;
  skippedDuplicate: number;
  skippedOutOfBounds: number;
  skippedNonResidential: number;
  skippedInvalid: number;
  currentFile: string;
  provinceName: string;
  error?: string;
  result?: ServerImportResult;
}

export async function writeProgress(importId: string, progress: ImportProgress): Promise<void> {
  try {
    await redis.set(`nar-import:${importId}`, JSON.stringify(progress), 'EX', PROGRESS_TTL);
  } catch (err) {
    logger.warn('Failed to write NAR import progress to Redis', err);
  }
}

export const narImportService = {
  /** List available NAR datasets grouped by province */
  async listDatasets(): Promise<{ narDir: string | null; datasets: NarDataset[] }> {
    const baseDir = getDataDir();
    const narDir = findNarDirectory(baseDir);
    if (!narDir) return { narDir: null, datasets: [] };

    // Validate directory paths to prevent traversal attacks
    const addressDir = await validateFilePath(baseDir, path.relative(baseDir, path.join(narDir, 'Addresses')));
    const locationDir = await validateFilePath(baseDir, path.relative(baseDir, path.join(narDir, 'Locations')));
    const files: NarFileInfo[] = [];

    if (fs.existsSync(addressDir)) {
      for (const f of fs.readdirSync(addressDir)) {
        // Validate each file path
        const filePath = await validateFilePath(addressDir, f);
        const info = parseFileInfo(filePath, f);
        if (info) files.push(info);
      }
    }
    if (fs.existsSync(locationDir)) {
      for (const f of fs.readdirSync(locationDir)) {
        // Validate each file path
        const filePath = await validateFilePath(locationDir, f);
        const info = parseFileInfo(filePath, f);
        if (info) files.push(info);
      }
    }

    // Group by province
    const byProvince = new Map<string, NarDataset>();
    for (const file of files) {
      let dataset = byProvince.get(file.provinceCode);
      if (!dataset) {
        dataset = {
          provinceCode: file.provinceCode,
          provinceName: file.provinceName,
          provinceAbbr: file.provinceAbbr,
          addressFiles: [],
          locationFiles: [],
          totalAddressSize: 0,
          totalLocationSize: 0,
        };
        byProvince.set(file.provinceCode, dataset);
      }
      if (file.type === 'address') {
        dataset.addressFiles.push(file);
        dataset.totalAddressSize += file.sizeBytes;
      } else {
        dataset.locationFiles.push(file);
        dataset.totalLocationSize += file.sizeBytes;
      }
    }

    // Sort files within each dataset, sort datasets by province code
    const datasets = Array.from(byProvince.values());
    for (const ds of datasets) {
      ds.addressFiles.sort((a, b) => (a.partNumber ?? 0) - (b.partNumber ?? 0));
      ds.locationFiles.sort((a, b) => (a.partNumber ?? 0) - (b.partNumber ?? 0));
    }
    datasets.sort((a, b) => a.provinceCode.localeCompare(b.provinceCode));

    return { narDir, datasets };
  },

  /**
   * Load Location file(s) into a lookup Map for a given province.
   * Returns Map<LOC_GUID, { lat, lng, fedDistrict, fedCode }>.
   */
  async loadLocationLookup(locationFiles: NarFileInfo[]): Promise<Map<string, LocationLookup>> {
    const lookup = new Map<string, LocationLookup>();

    for (const file of locationFiles) {
      logger.info(`Loading location lookup from ${file.filename} (${file.sizeFormatted})`);

      const parser = fs.createReadStream(file.fullPath).pipe(
        parse({ columns: true, skip_empty_lines: true, trim: true, bom: true })
      );

      for await (const record of parser) {
        const locGuid = (record.LOC_GUID ?? '').trim();
        if (!locGuid) continue;

        const lat = parseFloat(record.BG_LATITUDE ?? '');
        const lng = parseFloat(record.BG_LONGITUDE ?? '');
        if (isNaN(lat) || isNaN(lng)) continue;

        lookup.set(locGuid, {
          lat,
          lng,
          fedDistrict: (record.FED_ENG_NAME ?? '').trim() || undefined,
          fedCode: (record.FED_CODE ?? '').trim() || undefined,
        });
      }

      logger.info(`Loaded ${lookup.size} locations from ${file.filename}`);
    }

    return lookup;
  },

  /** Import a province's NAR data using Location+Address file join */
  async importProvince(
    userId: string,
    options: ServerImportOptions,
    importId?: string,
  ): Promise<ServerImportResult> {
    const startTime = Date.now();
    const { narDir, datasets } = await this.listDatasets();

    if (!narDir) {
      throw new Error('NAR data directory not found. Place NAR data in ' + getDataDir());
    }

    const dataset = datasets.find((d) => d.provinceCode === options.provinceCode);
    if (!dataset) {
      throw new Error(`Province ${options.provinceCode} not found in NAR data`);
    }

    if (dataset.addressFiles.length === 0) {
      throw new Error(`No address files found for province ${options.provinceCode}`);
    }

    const result: ServerImportResult = {
      provinceCode: dataset.provinceCode,
      provinceName: dataset.provinceName,
      totalRows: 0,
      locationsCreated: 0,
      addressesCreated: 0,
      skippedDuplicate: 0,
      skippedOutOfBounds: 0,
      skippedNonResidential: 0,
      skippedInvalid: 0,
      errors: [],
      durationMs: 0,
    };

    // Helper to write progress if importId is provided
    const progress: ImportProgress = {
      status: 'loading-locations',
      totalRows: 0,
      locationsCreated: 0,
      addressesCreated: 0,
      skippedDuplicate: 0,
      skippedOutOfBounds: 0,
      skippedNonResidential: 0,
      skippedInvalid: 0,
      currentFile: '',
      provinceName: dataset.provinceName,
    };

    const updateProgress = async (updates?: Partial<ImportProgress>) => {
      if (!importId) return;
      if (updates) Object.assign(progress, updates);
      await writeProgress(importId, progress);
    };

    // Phase 1: Load Location file(s) for coordinate + federal district lookup
    await updateProgress();
    let locationLookup: Map<string, LocationLookup>;
    if (dataset.locationFiles.length > 0) {
      locationLookup = await this.loadLocationLookup(dataset.locationFiles);
      logger.info(`Location lookup ready: ${locationLookup.size} entries for ${dataset.provinceName}`);
    } else {
      locationLookup = new Map();
      logger.warn(`No Location files for ${dataset.provinceName}, will use proj4 coordinate conversion`);
    }

    // Load cut polygon if filtering by cut
    let cutPolygon: number[][][] | undefined;
    if (options.filterType === 'cut' && options.cutId) {
      const cut = await prisma.cut.findUnique({ where: { id: options.cutId } });
      if (cut) {
        try {
          cutPolygon = parseGeoJsonPolygon(cut.geojson);
        } catch {
          logger.warn(`Invalid cut polygon for ${options.cutId}`);
        }
      }
    }

    // Load existing coordinates for deduplication
    const existingCoords = new Set<string>();
    if (options.deduplicateRadius > 0) {
      const existing = await prisma.location.findMany({
        select: { latitude: true, longitude: true },
      }); // No where clause needed - latitude/longitude are non-nullable
      for (const loc of existing) {
        const key = `${roundCoord(Number(loc.latitude))}:${roundCoord(Number(loc.longitude))}`;
        existingCoords.add(key);
      }
    }
    const inFileCoords = new Set<string>();

    // Phase 2: Stream Address file(s) and group by LOC_GUID
    await updateProgress({ status: 'importing' });

    interface AddressRecord {
      addrGuid: string;
      locGuid: string;
      unitNumber?: string;
      civicNo: string;
      civicSuffix: string;
      streetName: string;
      streetType: string;
      streetDir: string;
      city: string;
      prov: string;
      postalCode?: string;
      buUse?: number;
      lat?: number;
      lng?: number;
      federalDistrict?: string;
    }

    const addressesByLocGuid = new Map<string, AddressRecord[]>();

    for (const addressFile of dataset.addressFiles) {
      logger.info(`Processing ${addressFile.filename} (${addressFile.sizeFormatted})`);
      await updateProgress({ currentFile: addressFile.filename });

      const parser = fs.createReadStream(addressFile.fullPath).pipe(
        parse({ columns: true, skip_empty_lines: true, trim: true, bom: true })
      );

      for await (const record of parser) {
        result.totalRows++;

        try {
          const locGuid = (record.LOC_GUID ?? '').trim();
          if (!locGuid) {
            result.skippedInvalid++;
            continue;
          }

          // Extract BU_USE for residential filter
          const buUse = parseInt(record.BU_USE ?? '', 10);
          if (options.residentialOnly && buUse === 3) {
            result.skippedNonResidential++;
            continue;
          }

          // Extract address components
          const civicNo = (record.CIVIC_NO ?? '').trim();
          const civicSuffix = (record.CIVIC_NO_SUFFIX ?? '').trim();
          const streetName = (record.OFFICIAL_STREET_NAME ?? '').trim();
          const streetType = (record.OFFICIAL_STREET_TYPE ?? '').trim();
          const streetDir = (record.OFFICIAL_STREET_DIR ?? '').trim();
          const city = (record.MAIL_MUN_NAME ?? record.CSD_ENG_NAME ?? '').trim();
          const prov = (record.MAIL_PROV_ABVN ?? '').trim();
          const postalCode = (record.MAIL_POSTAL_CODE ?? '').trim() || undefined;
          const unitNumber = (record.APT_NO_LABEL ?? '').trim() || undefined;
          const addrGuid = (record.ADDR_GUID ?? '').trim();

          // City filter
          if (options.filterType === 'city' && options.filterCity) {
            if (city.toLowerCase() !== options.filterCity.toLowerCase()) {
              result.skippedOutOfBounds++;
              continue;
            }
          }

          // Postal prefix filter
          if (options.filterType === 'postalPrefix' && options.filterPostalPrefix && postalCode) {
            if (!postalCode.toUpperCase().startsWith(options.filterPostalPrefix.toUpperCase())) {
              result.skippedOutOfBounds++;
              continue;
            }
          } else if (options.filterType === 'postalPrefix' && options.filterPostalPrefix && !postalCode) {
            result.skippedOutOfBounds++;
            continue;
          }

          if (!streetName) {
            result.skippedInvalid++;
            continue;
          }

          // Get coordinates + federal district from Location lookup
          let lat: number | undefined;
          let lng: number | undefined;
          let federalDistrict: string | undefined;

          const locData = locationLookup.get(locGuid);
          if (locData) {
            lat = locData.lat;
            lng = locData.lng;
            federalDistrict = locData.fedDistrict;
          } else {
            // Fallback: convert BG_X/BG_Y via proj4
            const bgX = parseFloat(record.BG_X ?? '');
            const bgY = parseFloat(record.BG_Y ?? '');
            if (!isNaN(bgX) && !isNaN(bgY)) {
              const [projLng, projLat] = proj4('EPSG:3347', 'EPSG:4326', [bgX, bgY]);
              lat = projLat;
              lng = projLng;
            }
          }

          if (lat === undefined || lng === undefined) {
            result.skippedInvalid++;
            continue;
          }

          // Cut boundary filter
          if (cutPolygon && cutPolygon.length > 0) {
            const inside = cutPolygon.some((ring) => isPointInPolygon(lat!, lng!, ring));
            if (!inside) {
              result.skippedOutOfBounds++;
              continue;
            }
          }

          // Group addresses by LOC_GUID
          if (!addressesByLocGuid.has(locGuid)) {
            addressesByLocGuid.set(locGuid, []);
          }
          addressesByLocGuid.get(locGuid)!.push({
            addrGuid,
            locGuid,
            unitNumber,
            civicNo,
            civicSuffix,
            streetName,
            streetType,
            streetDir,
            city,
            prov,
            postalCode,
            buUse: isNaN(buUse) ? undefined : buUse,
            lat,
            lng,
            federalDistrict,
          });

          // Write progress to Redis every PROGRESS_INTERVAL rows
          if (result.totalRows % PROGRESS_INTERVAL === 0) {
            await updateProgress({
              totalRows: result.totalRows,
              locationsCreated: result.locationsCreated,
              addressesCreated: result.addressesCreated,
              skippedDuplicate: result.skippedDuplicate,
              skippedOutOfBounds: result.skippedOutOfBounds,
              skippedNonResidential: result.skippedNonResidential,
              skippedInvalid: result.skippedInvalid,
            });
          }
        } catch (err) {
          result.skippedInvalid++;
          if (result.errors.length < 50) {
            const msg = err instanceof Error ? err.message : 'Unknown error';
            result.errors.push(`Row ${result.totalRows}: ${msg}`);
          }
        }
      }
    }

    // Phase 3: Create Locations and Addresses in batches
    logger.info(`Creating locations and addresses from ${addressesByLocGuid.size} unique buildings`);
    await updateProgress({ status: 'creating-records' });

    const locationBatch: Prisma.LocationCreateManyInput[] = [];
    const addressBatch: Prisma.AddressCreateManyInput[] = [];
    const locationIdByGuid = new Map<string, string>();

    // Helper function to build base address without unit number
    const buildBaseAddress = (addr: AddressRecord): string => {
      const streetParts = [
        addr.civicNo + (addr.civicSuffix || ''),
        addr.streetName,
        addr.streetType,
        addr.streetDir,
      ].filter(Boolean);
      let baseAddr = streetParts.join(' ');
      if (addr.city) baseAddr += `, ${addr.city}`;
      if (addr.prov) baseAddr += `, ${addr.prov}`;
      return baseAddr;
    };

    for (const [locGuid, addressRecords] of addressesByLocGuid.entries()) {
      const firstRecord = addressRecords[0]!;

      // Deduplication check (using first address coordinates)
      if (options.deduplicateRadius > 0) {
        const coordKey = `${roundCoord(firstRecord.lat!)}:${roundCoord(firstRecord.lng!)}`;
        if (existingCoords.has(coordKey) || inFileCoords.has(coordKey)) {
          result.skippedDuplicate += addressRecords.length;
          continue;
        }
        inFileCoords.add(coordKey);
      }

      // Create Location (one per LOC_GUID)
      const locationId = `loc_${Date.now()}_${Math.random().toString(36).substring(7)}`;

      locationBatch.push({
        id: locationId,
        latitude: firstRecord.lat!,
        longitude: firstRecord.lng!,
        address: buildBaseAddress(firstRecord),
        postalCode: firstRecord.postalCode,
        province: firstRecord.prov || undefined,
        federalDistrict: firstRecord.federalDistrict,
        buildingUse: firstRecord.buUse,
        locGuid,
        buildingType: addressRecords.length > 1 ? 'MULTI_UNIT' : 'SINGLE_FAMILY',
        totalUnits: addressRecords.length,
        createdByUserId: userId,
      });

      locationIdByGuid.set(locGuid, locationId);

      // Create Addresses (one per unit)
      for (const record of addressRecords) {
        addressBatch.push({
          id: `addr_${Date.now()}_${Math.random().toString(36).substring(7)}`,
          locationId,
          unitNumber: record.unitNumber,
          addrGuid: record.addrGuid,
          createdByUserId: userId,
        });
      }

      // Batch insert every 1000 locations
      if (locationBatch.length >= 1000) {
        await prisma.location.createMany({ data: locationBatch, skipDuplicates: true });
        await prisma.address.createMany({ data: addressBatch, skipDuplicates: true });
        result.locationsCreated += locationBatch.length;
        result.addressesCreated += addressBatch.length;
        locationBatch.length = 0;
        addressBatch.length = 0;

        logger.info(`Progress: ${result.locationsCreated} locations, ${result.addressesCreated} addresses created`);
      }
    }

    // Flush remaining batches
    if (locationBatch.length > 0) {
      await prisma.location.createMany({ data: locationBatch, skipDuplicates: true });
      await prisma.address.createMany({ data: addressBatch, skipDuplicates: true });
      result.locationsCreated += locationBatch.length;
      result.addressesCreated += addressBatch.length;
    }

    result.durationMs = Date.now() - startTime;
    logger.info(
      `NAR import complete for ${dataset.provinceName}: ${result.locationsCreated} locations, ${result.addressesCreated} addresses created from ${result.totalRows} rows in ${(result.durationMs / 1000).toFixed(1)}s`
    );

    await updateProgress({
      status: 'complete',
      totalRows: result.totalRows,
      locationsCreated: result.locationsCreated,
      addressesCreated: result.addressesCreated,
      skippedDuplicate: result.skippedDuplicate,
      skippedOutOfBounds: result.skippedOutOfBounds,
      skippedNonResidential: result.skippedNonResidential,
      skippedInvalid: result.skippedInvalid,
      result,
    });

    return result;
  },
};
