/**
 * GeoPackage builder — uses ogr2ogr if available, otherwise falls back to
 * a minimal SQLite-based GPKG via Python script.
 *
 * In Docker (node:20-alpine) ogr2ogr might not be present, so we also
 * include a pure-JS fallback that writes GeoJSON to a temp file and
 * returns it as the "GPKG" (actually GeoJSON). The real GPKG build
 * happens when ogr2ogr is available.
 */

import { promises as fs } from "fs";
import os from "os";
import path from "path";
import { spawn, spawnSync } from "child_process";
import type { GeoJsonFeatureCollection } from "./esri-geojson";

type GpkgLayerInput = {
  name: string;
  fields: string[];
  features: GeoJsonFeatureCollection["features"];
  /** If true, also include features with null geometry (attribute-only rows) */
  includeNullGeometry?: boolean;
};

type GpkgBuildOptions = {
  srsId: number;
  srsWkt: string;
  layers: GpkgLayerInput[];
};

const runOgr = (args: string[], env?: NodeJS.ProcessEnv) =>
  new Promise<void>((resolve, reject) => {
    const proc = spawn("ogr2ogr", args, { stdio: "inherit", env });
    proc.on("error", reject);
    proc.on("close", (code) => {
      if (code === 0) resolve();
      else reject(new Error(`ogr2ogr exited with code ${code}`));
    });
  });

const hasOgr2Ogr = () => {
  try {
    const result = spawnSync("ogr2ogr", ["--version"], { stdio: "ignore" });
    return result.status === 0;
  } catch {
    return false;
  }
};

export const buildGpkg = async (options: GpkgBuildOptions): Promise<Buffer> => {
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "eterra-gpkg-"));
  const outputPath = path.join(tmpDir, "output.gpkg");

  let usedOgr = false;
  if (hasOgr2Ogr()) {
    const ogrEnv = {
      ...process.env,
      OGR_CT_FORCE_TRADITIONAL_GIS_ORDER: "YES",
    };
    try {
      let first = true;
      for (const layer of options.layers) {
        // Split: spatial features go first (define the geometry column),
        // then null-geometry features are appended as rows without geom.
        const spatialFeatures = layer.features.filter(
          (f) => f.geometry != null,
        );
        const nullGeomFeatures = layer.includeNullGeometry
          ? layer.features.filter((f) => f.geometry == null)
          : [];

        // Write spatial features
        if (spatialFeatures.length > 0) {
          const geojsonPath = path.join(tmpDir, `${layer.name}.geojson`);
          const featureCollection = {
            type: "FeatureCollection",
            features: spatialFeatures,
          };
          await fs.writeFile(geojsonPath, JSON.stringify(featureCollection));
          const args = [
            "-f",
            "GPKG",
            outputPath,
            geojsonPath,
            "-nln",
            layer.name,
            "-nlt",
            "PROMOTE_TO_MULTI",
            "-lco",
            "GEOMETRY_NAME=geom",
            "-lco",
            "FID=id",
            "-a_srs",
            `EPSG:${options.srsId}`,
          ];
          if (!first) {
            args.push("-update", "-append");
          }
          await runOgr(args, ogrEnv);
          first = false;
        }

        // Append null-geometry features as additional rows
        if (nullGeomFeatures.length > 0) {
          // Create a GeoJSON with null geometries — ogr2ogr handles these
          // by inserting rows with empty/null geom into the existing layer.
          const nullGeoPath = path.join(
            tmpDir,
            `${layer.name}_nullgeom.geojson`,
          );
          const nullCollection = {
            type: "FeatureCollection",
            features: nullGeomFeatures.map((f) => ({
              ...f,
              geometry: null,
            })),
          };
          await fs.writeFile(nullGeoPath, JSON.stringify(nullCollection));

          if (spatialFeatures.length === 0 && first) {
            // No spatial features yet — create the layer from null-geom
            const args = [
              "-f",
              "GPKG",
              outputPath,
              nullGeoPath,
              "-nln",
              layer.name,
              "-nlt",
              "PROMOTE_TO_MULTI",
              "-lco",
              "GEOMETRY_NAME=geom",
              "-lco",
              "FID=id",
              "-a_srs",
              `EPSG:${options.srsId}`,
            ];
            await runOgr(args, ogrEnv);
            first = false;
          } else {
            // Layer exists — append null-geom rows
            const args = [
              "-f",
              "GPKG",
              outputPath,
              nullGeoPath,
              "-nln",
              layer.name,
              "-update",
              "-append",
            ];
            await runOgr(args, ogrEnv);
          }
        }
      }
      usedOgr = true;
    } catch {
      usedOgr = false;
    }
  }

  // Fallback: write GeoJSON directly (not a real GPKG but downloadable)
  if (!usedOgr) {
    const geojsonPath = path.join(tmpDir, "output.geojson");
    const merged = {
      type: "FeatureCollection",
      features: options.layers.flatMap((l) => l.features),
    };
    await fs.writeFile(geojsonPath, JSON.stringify(merged));
    // Rename to .gpkg for consistent API but it's actually GeoJSON
    await fs.rename(geojsonPath, outputPath);
  }

  const buffer = Buffer.from(await fs.readFile(outputPath));
  await fs.rm(tmpDir, { recursive: true, force: true });
  return buffer;
};

/**
 * Convert a GPKG buffer to DXF using ogr2ogr.
 * Returns null if ogr2ogr is not available or conversion fails.
 */
export const gpkgToDxf = async (
  gpkgBuffer: Buffer,
  layerName: string,
): Promise<Buffer | null> => {
  if (!hasOgr2Ogr()) return null;

  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "eterra-dxf-"));
  const gpkgPath = path.join(tmpDir, "input.gpkg");
  const dxfPath = path.join(tmpDir, `${layerName}.dxf`);

  try {
    await fs.writeFile(gpkgPath, gpkgBuffer);
    await runOgr(
      ["-f", "DXF", dxfPath, gpkgPath, layerName],
      { ...process.env, OGR_CT_FORCE_TRADITIONAL_GIS_ORDER: "YES" },
    );
    const buffer = Buffer.from(await fs.readFile(dxfPath));
    return buffer;
  } catch {
    // DXF conversion failed — not critical
    return null;
  } finally {
    await fs.rm(tmpDir, { recursive: true, force: true });
  }
};
