/**
 * Remote Database Module
 *
 * Handles all API communication with the PostgreSQL backend server.
 * Provides GET and POST methods for fetching and pushing data.
 *
 * Usage:
 *   import { remoteGet, remotePost, getDistrictBoundary } from './remotedb.js';
 *
 *   const boundary = await getDistrictBoundary();
 */

// ============================================================================
// Configuration
// ============================================================================

const API_BASE = 'https://api.lupmis4luspa.org/api/spatial_planning';

/**
 * Per-request credentials sent with every API call.
 *
 * `district_id` is resolved dynamically — when the PWA is loaded via the PHP
 * entry point (public/index.php), the SSO session is injected into the page
 * as `window.LUPMIS_SESSION` and we read the authenticated user's district
 * from there. In local development (Vite serves index.html directly without
 * PHP), the global is undefined and we fall back to the hard-coded test
 * district below.
 *
 * `api_token` is currently a single global app token — not per-user.
 */
const FALLBACK_DISTRICT_ID = '1';
const API_TOKEN = '1c46538c712e9b5b';

/**
 * Returns the authenticated user's district_id.
 *
 *  - No SSO session at all (window.LUPMIS_SESSION undefined): we're in local
 *    development → fall back to the hard-coded test district.
 *  - Session present but no district_id: the user is authenticated but not
 *    assigned to any district → return null. The bootstrap in main.js detects
 *    this case BEFORE any API call and shows a blocking message; this null
 *    is defence-in-depth so we never silently send district_id=1 for an
 *    authenticated user.
 *
 * The getter runs on each spread of API_CREDENTIALS, so a session change at
 * runtime takes effect immediately.
 */
function resolveDistrictId() {
  try {
    if (typeof window === 'undefined') return FALLBACK_DISTRICT_ID;
    const session = window.LUPMIS_SESSION;
    if (!session || typeof session !== 'object') return FALLBACK_DISTRICT_ID;
    const id = session.district_id;
    if (id === null || id === undefined || String(id).length === 0) return null;
    return String(id);
  } catch { /* no-op */ }
  return FALLBACK_DISTRICT_ID;
}

const API_CREDENTIALS = {
  get district_id() { return resolveDistrictId(); },
  api_token: API_TOKEN,
};

/**
 * Get the full session payload (or null if not authenticated).
 * Exposed for UI code that wants to display the user's name, email, etc.
 *
 * Dev-mode helper: setting `localStorage['dev-session']` to a JSON object
 * (e.g. via `lupmisDevSession({...})` in the console) overrides the real
 * session — useful when running against `localhost:5173` to test the
 * authenticated UI without standing up a PHP server.
 */
export function getSession() {
  // 1. Real session injected by index.php (production)
  if (typeof window !== 'undefined' && window.LUPMIS_SESSION && window.LUPMIS_SESSION.user_id) {
    return window.LUPMIS_SESSION;
  }
  // 2. Dev-mode override (developer's own localStorage tweak)
  try {
    const raw = localStorage.getItem('dev-session');
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && parsed.user_id) return parsed;
    }
  } catch { /* ignore */ }
  return null;
}

// Console helper — set a fake session for dev work. Reload to apply.
//   lupmisDevSession({ user_id: 42, district_id: '1', full_name: 'Test User', ... })
// Clear it via lupmisDevSession(null) or localStorage.removeItem('dev-session').
if (typeof window !== 'undefined') {
  window.lupmisDevSession = (payload) => {
    if (payload == null) {
      localStorage.removeItem('dev-session');
      console.log('[Dev] Session override cleared. Reload to apply.');
    } else {
      localStorage.setItem('dev-session', JSON.stringify(payload));
      console.log('[Dev] Session override saved. Reload to apply:', payload);
    }
  };
}

// ============================================================================
// Server Reachability
// ============================================================================

/** Default timeout for API requests (ms) */
const REQUEST_TIMEOUT = 30_000;

/** Timeout for the fast reachability probe (ms) */
const PING_TIMEOUT = 5_000;

/** Cached result of the last reachability check */
let _serverReachable = null;

/**
 * Quick probe to determine if the API server is responding.
 * Sends a small POST to a lightweight endpoint with a short timeout.
 * The result is cached so subsequent calls within the same page load
 * return immediately.
 *
 * @param {boolean} [force=false] - Re-check even if a cached result exists
 * @returns {Promise<boolean>} true if the server responded in time
 */
export async function checkServerReachable(force = false) {
  if (_serverReachable !== null && !force) return _serverReachable;

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), PING_TIMEOUT);

  try {
    const response = await fetch(`${API_BASE}/get_layers.php`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
      body: JSON.stringify(API_CREDENTIALS),
      signal: controller.signal,
    });
    _serverReachable = response.ok;
  } catch {
    _serverReachable = false;
  } finally {
    clearTimeout(timer);
  }

  console.log('[RemoteDB] Server reachable:', _serverReachable);
  return _serverReachable;
}

/**
 * Returns the cached server-reachability flag (synchronous).
 * Returns null if checkServerReachable() has not been called yet.
 * @returns {boolean|null}
 */
export function isServerReachable() {
  return _serverReachable;
}

// ============================================================================
// Core Request Helpers
// ============================================================================

/**
 * Create an AbortController that auto-aborts after `ms` milliseconds.
 * If the caller already supplied a signal in `options`, it is combined
 * so that either the caller's abort or the timeout will cancel the request.
 */
function withTimeout(options, ms = REQUEST_TIMEOUT) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  // If the caller provided their own signal, chain it
  if (options.signal) {
    options.signal.addEventListener('abort', () => controller.abort());
  }

  return {
    signal: controller.signal,
    clear: () => clearTimeout(timer),
  };
}

/**
 * Perform a GET request to the remote API.
 * Credentials are sent as URL query parameters.
 * Automatically times out after REQUEST_TIMEOUT ms.
 *
 * @param {string} endpoint - API endpoint filename (e.g. 'get_district_boundary.php')
 * @param {Object} [params={}] - Additional query parameters
 * @param {Object} [options={}] - Extra fetch options
 * @returns {Promise<Object>} Parsed JSON response
 */
export async function remoteGet(endpoint, params = {}, options = {}) {
  const url = new URL(`${API_BASE}/${endpoint}`);

  // Attach credentials and any extra params as query string
  const allParams = { ...API_CREDENTIALS, ...params };
  for (const [key, value] of Object.entries(allParams)) {
    url.searchParams.set(key, value);
  }

  console.log('[RemoteDB] GET', url.toString());

  const timeout = withTimeout(options);
  try {
    const response = await fetch(url.toString(), {
      method: 'GET',
      headers: {
        'Accept': 'application/json'
      },
      ...options,
      signal: timeout.signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();
    console.log('[RemoteDB] GET response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);
    return data;

  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('[RemoteDB] GET timed out:', endpoint);
      throw new Error(`Request timed out: ${endpoint}`);
    }
    console.error('[RemoteDB] GET failed:', endpoint, error);
    throw error;
  } finally {
    timeout.clear();
  }
}

/**
 * Perform a POST request to the remote API.
 * Credentials are included in the JSON body.
 * Automatically times out after REQUEST_TIMEOUT ms.
 *
 * @param {string} endpoint - API endpoint filename (e.g. 'some_endpoint.php')
 * @param {Object} [body={}] - Request payload (credentials are merged in)
 * @param {Object} [options={}] - Extra fetch options
 * @returns {Promise<Object>} Parsed JSON response
 */
export async function remotePost(endpoint, body = {}, options = {}) {
  const url = `${API_BASE}/${endpoint}`;

  const payload = { ...API_CREDENTIALS, ...body };

  console.log('[RemoteDB] POST', url);

  const timeout = withTimeout(options);
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(payload),
      ...options,
      signal: timeout.signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();
    console.log('[RemoteDB] POST response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);
    return data;

  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('[RemoteDB] POST timed out:', endpoint);
      throw new Error(`Request timed out: ${endpoint}`);
    }
    console.error('[RemoteDB] POST failed:', endpoint, error);
    throw error;
  } finally {
    timeout.clear();
  }
}

// ============================================================================
// Spatial Planning Endpoints
// ============================================================================

/**
 * Fetch district boundary geometry from the server.
 *
 * @returns {Promise<Object>} District boundary GeoJSON or API response
 */
export async function getDistrictBoundary() {
  return remotePost('get_district_boundary.php');
}

/**
 * Fetch the list of available map layer categories from the server.
 *
 * Response format:
 * { success: true, data: [{ id, name, description, createdt, editdt }, ...] }
 *
 * @returns {Promise<Object>} Layer categories list
 */
export async function getLayers() {
  return remotePost('get_layers.php');
}

/**
 * Fetch all collector zones for the current district.
 *
 * Expected response:
 * { success: true, data: [{ id, zone_name, boundary: "MULTIPOLYGON(...)", ... }, ...] }
 *
 * @returns {Promise<Object>} Collector zones list
 */
export async function getCollectorZones() {
  return remotePost('get_all_collector_zone_per_district.php');
}

/**
 * Fetch all parcels for the current district.
 *
 * Expected response:
 * { success: true, data: [{ id, ..., polygon: "POLYGON(...)" | "MULTIPOLYGON(...)", ... }, ...] }
 *
 * @returns {Promise<Object>} Parcels list
 */
export async function getDistrictParcels() {
  return remotePost('get_parcels_per_district.php');
}

/**
 * Fetch all building footprints for the current district.
 *
 * Expected response:
 * { success: true, data: [{ id, ..., polygon: "POLYGON(...)" | "MULTIPOLYGON(...)", ... }, ...] }
 *
 * @returns {Promise<Object>} Building footprints list
 */
export async function getBuildingFootprints() {
  return remotePost('get_all_footprint_per_district.php');
}

/**
 * Fetch the Contours hillshade elevation layer from the server.
 *
 * Source: table `be_contour_hillside` in the local PostgreSQL `public` schema
 * (imported from OpenTopography's viz.hh_hillshade).
 *
 * The current district_id is passed automatically via API_CREDENTIALS.
 *
 * Expected response:
 * { success: true, data: [{ id, elevation, geom: "LINESTRING(...)" | "MULTILINESTRING(...)" | "POLYGON(...)", ... }, ...] }
 *
 * @returns {Promise<Object>} Contours hillshade list
 */
export async function getContoursHillshade() {
  return remotePost('get_contours_hillshade.php');
}

/**
 * Fetch the OSM roads layer from the server.
 *
 * Source: table `pi_osm_roads` in the local PostgreSQL `public` schema
 * (imported from OpenStreetMap road network for the district).
 *
 * Expected response:
 * { success: true, data: [{ id, ..., geom: "LINESTRING(...)" | "MULTILINESTRING(...)", ... }, ...] }
 *
 * @returns {Promise<Object>} OSM roads list
 */
export async function getOSMRoads() {
  return remotePost('get_osm_roads.php');
}

/**
 * Fetch the UPN-grid (district sub-division) for the current district.
 *
 * The grid is static per district, so callers cache it locally and only
 * re-fetch when the user becomes associated with a different district
 * (see saveUpnGrid / getLocalUpnGrid in database.js and loadUpnGrid in
 * main.js).
 *
 * Source: spatial.upn_grid; endpoint runs ST_AsText(geom_4326) AS polygon.
 *
 * Expected response:
 * { success: true,
 *   data: [{ polygon: "POLYGON((...))" | "MULTIPOLYGON(((...)))",
 *            districtid: 122,
 *            upn_prefix: "AB" }, ...] }
 *
 * @returns {Promise<Object>}
 */
export async function getUpnGrid() {
  return remotePost('get_upn_grid_per_district.php');
}

/**
 * Push a recorded GPS trail (with all its points) to the server.
 *
 * Implements the GeoTracker "sync adapter" contract: store-and-forward — the
 * whole trail is uploaded once recording stops (and retried when back online).
 * `district_id` and `api_token` are attached automatically by remotePost().
 *
 * ── SERVER SIDE (NOT YET CREATED) ───────────────────────────────────────
 * Proposed endpoint: `save_gps_trail.php`
 * Proposed PostgreSQL/PostGIS tables (SRID 4326):
 *
 *   CREATE TABLE be_gps_trail (
 *     id           SERIAL PRIMARY KEY,
 *     client_uuid  TEXT UNIQUE,            -- de-dupe re-syncs
 *     district_id  INTEGER,
 *     name         TEXT,
 *     started_at   TIMESTAMPTZ,
 *     ended_at     TIMESTAMPTZ,
 *     point_count  INTEGER,
 *     distance_m   DOUBLE PRECISION,
 *     track        GEOMETRY(LineStringZ, 4326),  -- optional aggregate line
 *     createdt     TIMESTAMPTZ DEFAULT now()
 *   );
 *   CREATE TABLE be_gps_trail_point (
 *     id           SERIAL PRIMARY KEY,
 *     trail_id     INTEGER REFERENCES be_gps_trail(id) ON DELETE CASCADE,
 *     seq          INTEGER,
 *     geom         GEOMETRY(PointZ, 4326),
 *     accuracy     DOUBLE PRECISION,
 *     altitude     DOUBLE PRECISION,
 *     heading      DOUBLE PRECISION,
 *     speed        DOUBLE PRECISION,
 *     satellites   INTEGER,               -- nullable (web has no sat count)
 *     recorded_at  TIMESTAMPTZ
 *   );
 *
 * Request body (JSON, plus injected credentials):
 *   { client_uuid, name, started_at, ended_at, point_count, distance_m,
 *     points: [ { seq, longitude, latitude, altitude, accuracy,
 *                 altitude_accuracy, heading, speed, satellites, recorded_at } ] }
 *
 * Expected response: { success: true, id: <server trail id> }
 * Should be idempotent on client_uuid (INSERT ... ON CONFLICT DO UPDATE).
 * ─────────────────────────────────────────────────────────────────────────
 *
 * @param {Object} trail  local trail row (client_uuid, name, started_at, …)
 * @param {Array}  points local point rows (seq, longitude, latitude, …)
 * @returns {Promise<{ remoteId: (string|number|null) }>}
 */
export async function pushGpsTrail(trail, points) {
  const payload = {
    client_uuid: trail.client_uuid,
    name: trail.name ?? null,
    started_at: trail.started_at,
    ended_at: trail.ended_at,
    point_count: trail.point_count ?? points.length,
    distance_m: trail.distance_m ?? 0,
    points: (points || []).map((p) => ({
      seq: p.seq,
      longitude: p.longitude,
      latitude: p.latitude,
      altitude: p.altitude ?? null,
      accuracy: p.accuracy ?? null,
      altitude_accuracy: p.altitude_accuracy ?? null,
      heading: p.heading ?? null,
      speed: p.speed ?? null,
      satellites: p.satellites ?? null,
      recorded_at: p.recorded_at,
    })),
  };
  const res = await remotePost('save_gps_trail.php', payload);
  return { remoteId: res?.id ?? res?.remote_id ?? null };
}

// ============================================================================
// Exports
// ============================================================================

export default {
  getSession,
  checkServerReachable,
  isServerReachable,
  remoteGet,
  remotePost,
  getDistrictBoundary,
  getLayers,
  getDistrictParcels,
  getCollectorZones,
  getBuildingFootprints,
  getContoursHillshade,
  getOSMRoads,
  getUpnGrid,
  pushGpsTrail,
};
