/**
 * Sync engine — downloads eTerra features and stores them in PostgreSQL.
 *
 * Supports incremental sync: compares remote OBJECTIDs with local DB,
 * only downloads new features, marks removed ones.
 */

import { Prisma, PrismaClient } from "@prisma/client";
import { EterraClient } from "./eterra-client";
import type { LayerConfig, EsriFeature } from "./eterra-client";
import { esriToGeojson } from "./esri-geojson";
import { findLayerById, type LayerCatalogItem } from "./eterra-layers";
import { fetchUatGeometry } from "./uat-geometry";
import {
  setProgress,
  getProgress,
  clearProgress,
  type SyncProgress,
} from "./progress-store";

const prisma = new PrismaClient();

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export type SyncResult = {
  layerId: string;
  siruta: string;
  totalRemote: number;
  totalLocal: number;
  newFeatures: number;
  removedFeatures: number;
  /** Features with VALID_FROM changed (attribute update, no new OBJECTID) */
  validFromUpdated?: number;
  status: "done" | "error";
  error?: string;
};

/**
 * Sync a single layer for a UAT into the local GIS database.
 *
 * 1. Count remote features
 * 2. Get local OBJECTIDs already stored
 * 3. Download only new OBJECTIDs (incremental)
 * 4. Mark removed ones (present local, absent remote)
 * 5. Store results + sync run metadata
 */
export async function syncLayer(
  username: string,
  password: string,
  siruta: string,
  layerId: string,
  options?: {
    uatName?: string;
    jobId?: string;
    forceFullSync?: boolean;
    /** When true, don't set terminal status (done/error) on progress store.
     *  Used when syncLayer runs as a sub-step of a larger export flow. */
    isSubStep?: boolean;
    /** Override the default HTTP timeout for the eTerra client (ms). */
    timeoutMs?: number;
  },
): Promise<SyncResult> {
  const jobId = options?.jobId;
  const isSubStep = options?.isSubStep ?? false;
  const layer = findLayerById(layerId);
  if (!layer) throw new Error(`Layer ${layerId} not found`);

  const push = (partial: Partial<SyncProgress>) => {
    if (!jobId) return;
    if (isSubStep) {
      // When running as sub-step of a larger export, only write
      // phase/note/phaseCurrent/phaseTotal — preserve the parent's
      // downloaded/total weighted percentages.
      const existing = getProgress(jobId);
      setProgress({
        jobId,
        downloaded: existing?.downloaded ?? 0,
        total: existing?.total,
        status: "running",
        ...existing,
        // Only overwrite informational fields
        phase: partial.phase ?? existing?.phase,
        note: partial.note,
        phaseCurrent: partial.downloaded, // map sync's downloaded → sub-detail
        phaseTotal: partial.total, // map sync's total → sub-detail
        message: partial.message,
      } as SyncProgress);
      return;
    }
    setProgress({
      jobId,
      downloaded: 0,
      status: "running",
      ...partial,
    } as SyncProgress);
  };

  // Create sync run record
  const syncRun = await prisma.gisSyncRun.create({
    data: {
      siruta,
      uatName: options?.uatName,
      layerId,
      status: "running",
    },
  });

  try {
    push({ phase: "Conectare eTerra", downloaded: 0 });
    const client = await EterraClient.create(username, password, {
      timeoutMs: options?.timeoutMs,
    });

    // Get UAT geometry for spatial-filtered layers
    let uatGeometry;
    if (layer.spatialFilter) {
      push({ phase: "Obținere geometrie UAT" });
      uatGeometry = await fetchUatGeometry(client, siruta);
    }

    // Get local OBJECTIDs for this layer+siruta (only positive — skip no-geom)
    push({ phase: "Verificare locală" });
    const localFeatures = await prisma.gisFeature.findMany({
      where: { layerId, siruta, objectId: { gt: 0 } },
      select: { objectId: true },
    });
    const localObjIds = new Set(localFeatures.map((f) => f.objectId));

    // ── Quick-count check: if remote count == local count, skip full OBJECTID fetch ──
    // Just do VALID_FROM delta for attribute changes (handled after download section).
    let remoteCount = 0;
    let remoteObjIds = new Set<number>();
    let newObjIdArray: number[] = [];
    let removedObjIds: number[] = [];
    let useFullSync = false;
    let quickCountMatch = false;

    if (!options?.forceFullSync && localObjIds.size > 0) {
      push({ phase: "Verificare count remote" });
      let qCount = -1;
      try {
        qCount = uatGeometry
          ? await client.countLayerByGeometry(layer, uatGeometry)
          : await client.countLayer(layer, siruta);
      } catch {
        // Count check is best-effort — fall through to OBJECTID comparison
        qCount = -1;
      }

      if (qCount >= 0 && qCount === localObjIds.size) {
        // Counts match — very likely no new/removed features
        quickCountMatch = true;
        remoteCount = qCount;
        remoteObjIds = localObjIds; // Treat as identical
        newObjIdArray = [];
        removedObjIds = [];
        useFullSync = false;
        console.log(
          `[sync] Quick-count match: ${qCount} remote = ${localObjIds.size} local for ${layerId}/${siruta} — skipping OBJECTID fetch`,
        );
      }
    }

    if (!quickCountMatch) {
      // Full OBJECTID comparison (original path)
      push({ phase: "Comparare ID-uri remote" });
      let remoteObjIdArray: number[];
      try {
        remoteObjIdArray = uatGeometry
          ? await client.fetchObjectIdsByGeometry(layer, uatGeometry)
          : await client.fetchObjectIds(layer, siruta);
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.warn(
          `[syncLayer] fetchObjectIds failed for ${layerId}/${siruta}: ${msg} — falling back to full sync`,
        );
        remoteObjIdArray = [];
      }
      remoteObjIds = new Set(remoteObjIdArray);
      remoteCount = remoteObjIds.size;

      // Compute delta
      newObjIdArray = [...remoteObjIds].filter((id) => !localObjIds.has(id));
      removedObjIds = [...localObjIds].filter(
        (id) => !remoteObjIds.has(id),
      );

      // Decide: incremental (download only delta) or full sync
      const deltaRatio =
        remoteCount > 0 ? newObjIdArray.length / remoteCount : 1;
      useFullSync =
        options?.forceFullSync ||
        localObjIds.size === 0 ||
        deltaRatio > 0.5;
    }

    let allRemote: EsriFeature[];

    if (useFullSync) {
      // Full sync: download all features (first sync or forced)
      push({
        phase: "Descărcare features (complet)",
        downloaded: 0,
        total: remoteCount,
      });
      allRemote = uatGeometry
        ? await client.fetchAllLayerByGeometry(layer, uatGeometry, {
            total: remoteCount > 0 ? remoteCount : undefined,
            onProgress: (dl, tot) =>
              push({
                phase: "Descărcare features (complet)",
                downloaded: dl,
                total: tot,
              }),
            delayMs: 200,
          })
        : await client.fetchAllLayerByWhere(
            layer,
            await buildWhere(client, layer, siruta),
            {
              total: remoteCount > 0 ? remoteCount : undefined,
              onProgress: (dl, tot) =>
                push({
                  phase: "Descărcare features (complet)",
                  downloaded: dl,
                  total: tot,
                }),
              delayMs: 200,
            },
          );
    } else if (newObjIdArray.length > 0) {
      // Incremental sync: download only the new features
      push({
        phase: "Descărcare features noi",
        downloaded: 0,
        total: newObjIdArray.length,
      });
      const baseWhere = uatGeometry
        ? undefined
        : await buildWhere(client, layer, siruta);
      allRemote = await client.fetchFeaturesByObjectIds(
        layer,
        newObjIdArray,
        {
          baseWhere,
          onProgress: (dl, tot) =>
            push({
              phase: "Descărcare features noi",
              downloaded: dl,
              total: tot,
            }),
          delayMs: 200,
        },
      );
    } else {
      // Nothing new to download
      allRemote = [];
    }

    // Convert to GeoJSON for geometry storage
    const geojson = esriToGeojson(allRemote);
    const geojsonByObjId = new Map<number, (typeof geojson.features)[0]>();
    for (const f of geojson.features) {
      const objId = f.properties.OBJECTID as number | undefined;
      if (objId != null) geojsonByObjId.set(objId, f);
    }

    // For incremental sync, newObjIds = the delta we downloaded
    // For full sync, newObjIds = all remote (if forced) or only truly new
    const newObjIds = options?.forceFullSync
      ? remoteObjIds
      : new Set(newObjIdArray);

    push({
      phase: "Salvare în baza de date",
      downloaded: 0,
      total: newObjIds.size,
    });

    // Insert new features in batches
    let saved = 0;
    const BATCH_SIZE = 100;
    const newArray = [...newObjIds];
    for (let i = 0; i < newArray.length; i += BATCH_SIZE) {
      const batch = newArray.slice(i, i + BATCH_SIZE);
      const creates = batch
        .map((objId) => {
          const feature = allRemote.find(
            (f) => (f.attributes.OBJECTID as number) === objId,
          );
          if (!feature) return null;
          const geoFeature = geojsonByObjId.get(objId);
          const geom = geoFeature?.geometry;
          return {
            layerId,
            siruta,
            objectId: objId,
            inspireId:
              (feature.attributes.INSPIRE_ID as string | undefined) ?? null,
            cadastralRef:
              (feature.attributes.NATIONAL_CADASTRAL_REFERENCE as
                | string
                | undefined) ?? null,
            areaValue:
              typeof feature.attributes.AREA_VALUE === "number"
                ? feature.attributes.AREA_VALUE
                : null,
            isActive: feature.attributes.IS_ACTIVE !== 0,
            attributes: feature.attributes as Prisma.InputJsonValue,
            geometry: geom ? (geom as Prisma.InputJsonValue) : Prisma.JsonNull,
            syncRunId: syncRun.id,
          };
        })
        .filter(Boolean);

      // Use upsert to handle potential conflicts (force sync)
      for (const item of creates) {
        if (!item) continue;
        await prisma.gisFeature.upsert({
          where: {
            layerId_objectId: {
              layerId: item.layerId,
              objectId: item.objectId,
            },
          },
          create: item,
          update: {
            siruta: item.siruta,
            inspireId: item.inspireId,
            cadastralRef: item.cadastralRef,
            areaValue: item.areaValue,
            isActive: item.isActive,
            attributes: item.attributes,
            geometry: item.geometry,
            syncRunId: item.syncRunId,
            updatedAt: new Date(),
            // enrichment + enrichedAt preserved — not overwritten
          },
        });
      }
      saved += creates.length;
      push({
        phase: "Salvare în baza de date",
        downloaded: saved,
        total: newObjIds.size,
      });
    }

    // Populate native PostGIS geometry (safety net if trigger not installed)
    try {
      await prisma.$executeRaw`
        UPDATE "GisFeature"
        SET geom = ST_SetSRID(ST_GeomFromGeoJSON(geometry::text), 3844)
        WHERE "layerId" = ${layerId} AND siruta = ${siruta}
          AND geometry IS NOT NULL AND geom IS NULL`;
    } catch {
      // PostGIS not available yet — not critical, skip silently
    }

    // Mark removed features (batch to avoid PostgreSQL 32767 bind variable limit)
    // Safety: if remote returned very few features compared to local, the session
    // likely expired mid-sync — skip deletion to avoid wiping valid data.
    const removedRatio = localObjIds.size > 0 ? removedObjIds.length / localObjIds.size : 0;
    if (removedObjIds.length > 0 && removedRatio > 0.8 && localObjIds.size > 100) {
      console.warn(
        `[sync] SKIP delete: ${removedObjIds.length}/${localObjIds.size} features (${Math.round(removedRatio * 100)}%) ` +
        `would be removed for ${layerId}/${siruta} — likely stale remote data. Aborting deletion.`,
      );
    } else if (removedObjIds.length > 0) {
      push({ phase: "Marcare șterse" });
      const BATCH = 30_000;
      for (let i = 0; i < removedObjIds.length; i += BATCH) {
        await prisma.gisFeature.deleteMany({
          where: {
            layerId,
            siruta,
            objectId: { in: removedObjIds.slice(i, i + BATCH) },
          },
        });
      }
    }

    // ── VALID_FROM delta: detect attribute changes on existing features ──
    // Features whose VALID_FROM changed since our stored copy need re-enrichment.
    // This catches ownership/CF changes that don't create new OBJECTIDs.
    let validFromUpdated = 0;
    if (!useFullSync && newObjIdArray.length === 0 && removedObjIds.length === 0) {
      // Nothing new/removed — check if existing features changed via VALID_FROM
      // Fetch the max VALID_FROM we have stored locally
      const maxValidFrom = await prisma.$queryRawUnsafe<
        Array<{ max_vf: string | null }>
      >(
        `SELECT MAX((attributes->>'VALID_FROM')::bigint)::text as max_vf ` +
          `FROM "GisFeature" WHERE "layerId" = $1 AND siruta = $2 AND "objectId" > 0`,
        layerId,
        siruta,
      );
      const localMaxVf = maxValidFrom[0]?.max_vf;
      if (localMaxVf) {
        // Ask eTerra: any features with VALID_FROM > our max?
        const baseWhere = await buildWhere(client, layer, siruta);
        const vfWhere = `${baseWhere} AND VALID_FROM>${localMaxVf}`;
        try {
          const changed = uatGeometry
            ? await client.fetchAllLayerByWhere(
                layer,
                `VALID_FROM>${localMaxVf}`,
                {
                  outFields: "*",
                  returnGeometry: true,
                  delayMs: 200,
                  geometry: uatGeometry,
                },
              )
            : await client.fetchAllLayerByWhere(layer, vfWhere, {
                outFields: "*",
                returnGeometry: true,
                delayMs: 200,
              });
          if (changed.length > 0) {
            push({ phase: `Actualizare ${changed.length} parcele modificate` });
            const changedGeojson = esriToGeojson(changed);
            const changedGeoMap = new Map<
              number,
              (typeof changedGeojson.features)[0]
            >();
            for (const f of changedGeojson.features) {
              const objId = f.properties.OBJECTID as number | undefined;
              if (objId != null) changedGeoMap.set(objId, f);
            }
            for (const feature of changed) {
              const objId = feature.attributes.OBJECTID as number;
              if (!objId) continue;
              const geoFeature = changedGeoMap.get(objId);
              const geom = geoFeature?.geometry;
              await prisma.gisFeature.updateMany({
                where: { layerId, objectId: objId },
                data: {
                  attributes: feature.attributes as Prisma.InputJsonValue,
                  geometry: geom
                    ? (geom as Prisma.InputJsonValue)
                    : undefined,
                  enrichedAt: null, // Force re-enrichment
                  updatedAt: new Date(),
                },
              });
            }
            validFromUpdated = changed.length;
            console.log(
              `[sync] VALID_FROM delta: ${changed.length} features updated for ${layerId}/${siruta}`,
            );
          }
        } catch (err) {
          // Non-critical — VALID_FROM check is best-effort
          console.warn(
            `[sync] VALID_FROM check failed for ${layerId}/${siruta}:`,
            err instanceof Error ? err.message : err,
          );
        }
      }
    }

    // Update sync run
    const localCount = await prisma.gisFeature.count({
      where: { layerId, siruta },
    });
    await prisma.gisSyncRun.update({
      where: { id: syncRun.id },
      data: {
        status: "done",
        totalRemote: remoteCount,
        totalLocal: localCount,
        newFeatures: newObjIds.size,
        removedFeatures: removedObjIds.length,
        completedAt: new Date(),
      },
    });

    push({
      phase: "Sync finalizat",
      status: isSubStep ? "running" : "done",
      downloaded: remoteCount,
      total: remoteCount,
    });
    if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000);

    return {
      layerId,
      siruta,
      totalRemote: remoteCount,
      totalLocal: localCount,
      newFeatures: newObjIds.size,
      removedFeatures: removedObjIds.length,
      validFromUpdated,
      status: "done",
    };
  } catch (error) {
    const msg = error instanceof Error ? error.message : "Unknown error";
    await prisma.gisSyncRun.update({
      where: { id: syncRun.id },
      data: { status: "error", errorMessage: msg, completedAt: new Date() },
    });
    push({
      phase: "Eroare sync",
      status: isSubStep ? "running" : "error",
      message: msg,
    });
    if (jobId && !isSubStep) setTimeout(() => clearProgress(jobId), 60_000);
    return {
      layerId,
      siruta,
      totalRemote: 0,
      totalLocal: 0,
      newFeatures: 0,
      removedFeatures: 0,
      status: "error",
      error: msg,
    };
  }
}

/** Helper to build where clause outside the client */
async function buildWhere(
  client: EterraClient,
  layer: LayerConfig,
  siruta: string,
) {
  const fields = await client.getLayerFieldNames(layer);
  const preferred = [
    "ADMIN_UNIT_ID",
    "SIRUTA",
    "UAT_ID",
    "SIRUTA_UAT",
    "UAT_SIRUTA",
  ];
  const upper = fields.map((f) => f.toUpperCase());
  let adminField: string | null = null;
  for (const key of preferred) {
    const idx = upper.indexOf(key);
    if (idx >= 0) {
      adminField = fields[idx] ?? null;
      break;
    }
  }
  if (!adminField) return "1=1";
  if (!layer.whereTemplate) return `${adminField}=${siruta}`;
  const hasIsActive = fields.some((f) => f.toUpperCase() === "IS_ACTIVE");
  if (layer.whereTemplate.includes("IS_ACTIVE") && !hasIsActive)
    return `${adminField}=${siruta}`;
  return layer.whereTemplate
    .replace(/\{\{adminField\}\}/g, adminField)
    .replace(/\{\{siruta\}\}/g, siruta);
}

/**
 * Get sync status for all layers for a given UAT.
 */
export async function getSyncStatus(siruta: string) {
  const runs = await prisma.gisSyncRun.findMany({
    where: { siruta },
    orderBy: { startedAt: "desc" },
  });

  const counts = await prisma.gisFeature.groupBy({
    by: ["layerId"],
    where: { siruta },
    _count: { id: true },
  });

  const countMap: Record<string, number> = {};
  for (const c of counts) {
    countMap[c.layerId] = c._count.id;
  }

  return { runs, localCounts: countMap };
}
