import fs from 'node:fs';
import path from 'node:path';
import { transaction } from '../database.js';
import { config } from '../config.js';
import { HttpError } from '../middleware/errors.js';
import { parseGpx } from './gpx.js';
import { distanceMeters, routeMetrics } from './geo.js';
import {
  ensureRouteDirectories,
  moveUploadedFile,
  routeDirectory,
  softDeleteRouteDirectory,
  restoreRouteDirectory,
  resolveStoredFile,
  removeUpload,
  removeStoredFile
} from './storage.js';

const INSERT_POINT_SQL = 'INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)';
const UPDATE_METRICS_SQL = `UPDATE routes SET
  start_lat=?, start_lon=?, center_lat=?, center_lon=?, min_lat=?, min_lon=?, max_lat=?, max_lon=?,
  distance_m=?, elevation_gain_m=?, point_count=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`;

function trimEdgeHyphens(value) {
  let start = 0;
  let end = value.length;

  while (start < end && value[start] === '-') start += 1;
  while (end > start && value[end - 1] === '-') end -= 1;

  return value.slice(start, end);
}

function slugify(value) {
  const normalized = String(value).toLowerCase().normalize('NFKD');
  const withoutMarks = normalized.replaceAll(/[\u0300-\u036f]/g, '');
  const separated = withoutMarks.replaceAll(/[^a-z0-9]+/g, '-');
  const trimmed = trimEdgeHyphens(separated);
  return trimmed.slice(0, 80) || `route-${Date.now()}`;
}

const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;

function rowToRoute(row, includeDeleted = false) {
  if (!row || (!includeDeleted && row.status !== 'active')) return null;

  return {
    id: row.id,
    slug: row.slug,
    name: row.name,
    description: row.description,
    schoolName: row.school_name,
    status: row.status,
    start: row.start_lat == null ? null : { lat: row.start_lat, lon: row.start_lon },
    center: row.center_lat == null ? null : { lat: row.center_lat, lon: row.center_lon },
    bounds: row.min_lat == null
      ? null
      : { minLat: row.min_lat, minLon: row.min_lon, maxLat: row.max_lat, maxLon: row.max_lon },
    distanceM: row.distance_m,
    elevationGainM: row.elevation_gain_m,
    pointCount: row.point_count,
    gpxUrl: row.gpx_path && row.status === 'active' ? routeGpxUrl(row.id) : null,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
    deletedAt: row.deleted_at
  };
}

function routeComparator(origin) {
  if (origin) return (left, right) => left.proximityM - right.proximityM;
  return (left, right) => left.name.localeCompare(right.name, 'de');
}

function insertRoutePoints(db, routeId, points, offset = 0) {
  const insert = db.prepare(INSERT_POINT_SQL);
  points.forEach((point, index) => {
    insert.run(routeId, offset + index, point.lat, point.lon, point.elevation, point.recordedAt);
  });
}

function updateMetrics(db, routeId, metrics) {
  db.prepare(UPDATE_METRICS_SQL).run(
    metrics.startLat,
    metrics.startLon,
    metrics.centerLat,
    metrics.centerLon,
    metrics.minLat,
    metrics.minLon,
    metrics.maxLat,
    metrics.maxLon,
    metrics.distanceM,
    metrics.elevationGainM,
    metrics.pointCount,
    routeId
  );
}

function uniqueSlug(db, source) {
  const base = slugify(source);
  let candidate = base;
  let suffix = 2;

  while (db.prepare('SELECT 1 FROM routes WHERE slug = ?').get(candidate)) {
    candidate = `${base}-${suffix}`;
    suffix += 1;
  }

  return candidate;
}

function cleanupFailedRoute(db, routeId, gpxFile) {
  removeUpload(gpxFile);
  if (!routeId) return;

  db.prepare('DELETE FROM route_points WHERE route_id = ?').run(routeId);
  db.prepare('DELETE FROM routes WHERE id = ?').run(routeId);
  fs.rmSync(routeDirectory(routeId), { recursive: true, force: true });
}

function rewriteMediaPaths(db, routeId, sourcePrefix, targetPrefix) {
  db.prepare('UPDATE pois SET audio_path = REPLACE(audio_path, ?, ?) WHERE route_id = ? AND audio_path IS NOT NULL')
    .run(sourcePrefix, targetPrefix, routeId);
  db.prepare(`UPDATE poi_images SET path = REPLACE(path, ?, ?)
    WHERE poi_id IN (SELECT id FROM pois WHERE route_id = ?)`)
    .run(sourcePrefix, targetPrefix, routeId);
}

export function listRoutes(db, {
  lat,
  lon,
  radiusKm = config.defaultRouteRadiusKm,
  includeDeleted = false
}) {
  const where = includeDeleted ? '' : "WHERE status = 'active'";
  const rows = db.prepare(`SELECT * FROM routes ${where} ORDER BY name COLLATE NOCASE`).all();
  const origin = Number.isFinite(lat) && Number.isFinite(lon) ? { lat, lon } : null;
  const maximumDistance = radiusKm * 1000;

  return rows
    .map(row => {
      const route = rowToRoute(row, includeDeleted);
      const proximityM = origin && route?.start ? distanceMeters(origin, route.start) : null;
      return { ...route, proximityM };
    })
    .filter(route => !origin || route.proximityM <= maximumDistance)
    .sort(routeComparator(origin));
}

export function getRoute(db, id, includeDeleted = false) {
  const row = db.prepare('SELECT * FROM routes WHERE id = ?').get(id);
  const route = rowToRoute(row, includeDeleted);
  if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');

  const points = db.prepare(`SELECT sequence, lat, lon, elevation, recorded_at AS recordedAt
    FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
  return { ...route, points, pois: listPois(db, id) };
}

export function getRouteGpxFile(db, id) {
  const route = db.prepare("SELECT id, slug, gpx_path FROM routes WHERE id = ? AND status = 'active'").get(id);
  if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');

  return {
    absolutePath: resolveStoredFile(route.gpx_path),
    filename: `${route.slug}.gpx`
  };
}

export function createRoute(db, fields, gpxFile) {
  if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');

  let routeId;
  try {
    const parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
    const name = String(fields.name || parsed.name || '').trim();
    if (!name) throw new HttpError(400, 'Ein Streckenname ist erforderlich.');

    routeId = transaction(db, () => {
      const slug = uniqueSlug(db, fields.slug || name);
      const metrics = parsed.metrics;
      const result = db.prepare(`INSERT INTO routes
        (slug, name, description, school_name, gpx_path, start_lat, start_lon, center_lat, center_lon,
         min_lat, min_lon, max_lat, max_lon, distance_m, elevation_gain_m, point_count)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
        slug,
        name,
        fields.description || '',
        fields.schoolName || '',
        'pending',
        metrics.startLat,
        metrics.startLon,
        metrics.centerLat,
        metrics.centerLon,
        metrics.minLat,
        metrics.minLon,
        metrics.maxLat,
        metrics.maxLon,
        metrics.distanceM,
        metrics.elevationGainM,
        metrics.pointCount
      );
      const id = Number(result.lastInsertRowid);
      insertRoutePoints(db, id, parsed.points);
      return id;
    });

    ensureRouteDirectories(routeId);
    const gpxPath = moveUploadedFile(gpxFile, path.join(routeDirectory(routeId), 'route.gpx'));
    db.prepare('UPDATE routes SET gpx_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
      .run(gpxPath, routeId);
    return getRoute(db, routeId);
  } catch (error) {
    cleanupFailedRoute(db, routeId, gpxFile);
    throw error;
  }
}

export function updateRoute(db, id, fields, gpxFile) {
  const existing = db.prepare("SELECT * FROM routes WHERE id = ? AND status = 'active'").get(id);
  if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');

  const parsed = gpxFile ? parseGpx(fs.readFileSync(gpxFile.path, 'utf8')) : null;
  transaction(db, () => {
    db.prepare(`UPDATE routes SET name = ?, description = ?, school_name = ?, updated_at = CURRENT_TIMESTAMP
      WHERE id = ?`).run(
      fields.name ?? existing.name,
      fields.description ?? existing.description,
      fields.schoolName ?? existing.school_name,
      id
    );

    if (!parsed) return;
    db.prepare('DELETE FROM route_points WHERE route_id = ?').run(id);
    insertRoutePoints(db, id, parsed.points);
    updateMetrics(db, id, parsed.metrics);
  });

  if (gpxFile) moveUploadedFile(gpxFile, path.join(routeDirectory(id), 'route.gpx'));
  return getRoute(db, id);
}

export function appendRoute(db, id, gpxFile) {
  if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');

  const existing = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
  if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');

  const parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
  const current = db.prepare(`SELECT lat, lon, elevation, recorded_at AS recordedAt
    FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
  const allPoints = current.concat(parsed.points).map((point, sequence) => ({ ...point, sequence }));

  transaction(db, () => {
    insertRoutePoints(db, id, parsed.points, current.length);
    updateMetrics(db, id, routeMetrics(allPoints));
  });

  removeUpload(gpxFile);
  return getRoute(db, id);
}

export function listPois(db, routeId) {
  const pois = db.prepare('SELECT * FROM pois WHERE route_id = ? ORDER BY sequence, id').all(routeId);
  const images = db.prepare(`SELECT id, path, caption, sequence FROM poi_images
    WHERE poi_id = ? ORDER BY sequence, id`);

  return pois.map(poi => ({
    id: poi.id,
    routeId: poi.route_id,
    title: poi.title,
    description: poi.description,
    lat: poi.lat,
    lon: poi.lon,
    triggerRadiusM: poi.trigger_radius_m,
    sequence: poi.sequence,
    audioUrl: poi.audio_path ? audioUrl(poi.route_id, poi.id) : null,
    images: images.all(poi.id).map(image => ({
      id: image.id,
      caption: image.caption,
      sequence: image.sequence,
      url: pictureUrl(poi.route_id, poi.id, image.id)
    }))
  }));
}

export function getPoi(db, routeId, poiId) {
  const row = db.prepare(`SELECT p.id FROM pois p JOIN routes r ON r.id = p.route_id
    WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'`).get(poiId, routeId);
  if (!row) throw new HttpError(404, 'POI nicht gefunden.');
  return listPois(db, routeId).find(poi => poi.id === Number(poiId));
}

export function createPoi(db, routeId, fields) {
  const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(routeId);
  if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');

  const lat = Number.parseFloat(fields.lat);
  const lon = Number.parseFloat(fields.lon);
  if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
    throw new HttpError(400, 'Gültige POI-Koordinaten sind erforderlich.');
  }

  const result = db.prepare(`INSERT INTO pois
    (route_id, title, description, lat, lon, trigger_radius_m, sequence) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
    routeId,
    fields.title || 'Unbenannter POI',
    fields.description || '',
    lat,
    lon,
    Number.parseFloat(fields.triggerRadiusM || config.defaultPoiTriggerMeters),
    Number.parseInt(fields.sequence || '0', 10)
  );
  return listPois(db, routeId).find(poi => poi.id === Number(result.lastInsertRowid));
}

export function updatePoi(db, routeId, poiId, fields) {
  const existing = db.prepare(`SELECT p.*, r.status FROM pois p JOIN routes r ON r.id = p.route_id
    WHERE p.id = ? AND p.route_id = ?`).get(poiId, routeId);
  if (existing?.status !== 'active') throw new HttpError(404, 'POI nicht gefunden.');

  const lat = fields.lat == null ? existing.lat : Number.parseFloat(fields.lat);
  const lon = fields.lon == null ? existing.lon : Number.parseFloat(fields.lon);
  const radius = fields.triggerRadiusM == null
    ? existing.trigger_radius_m
    : Number.parseFloat(fields.triggerRadiusM);
  const sequence = fields.sequence == null ? existing.sequence : Number.parseInt(fields.sequence, 10);
  if (![lat, lon, radius].every(Number.isFinite) || !Number.isInteger(sequence) || sequence < 0) {
    throw new HttpError(400, 'POI-Koordinaten, Auslöseradius oder Reihenfolge sind ungültig.');
  }

  db.prepare(`UPDATE pois SET title=?, description=?, lat=?, lon=?, trigger_radius_m=?, sequence=?,
    updated_at=CURRENT_TIMESTAMP WHERE id=?`).run(
    fields.title ?? existing.title,
    fields.description ?? existing.description,
    lat,
    lon,
    radius,
    sequence,
    poiId
  );
  return listPois(db, existing.route_id).find(poi => poi.id === Number(poiId));
}

export function deletePoi(db, routeId, poiId) {
  const existing = db.prepare(`SELECT p.id, p.audio_path FROM pois p JOIN routes r ON r.id = p.route_id
    WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'`).get(poiId, routeId);
  if (!existing) throw new HttpError(404, 'POI nicht gefunden.');

  const images = db.prepare('SELECT path FROM poi_images WHERE poi_id = ?').all(poiId);
  transaction(db, () => {
    db.prepare('DELETE FROM poi_images WHERE poi_id = ?').run(poiId);
    db.prepare('DELETE FROM pois WHERE id = ? AND route_id = ?').run(poiId, routeId);
  });

  images.forEach(image => removeStoredFile(image.path));
  removeStoredFile(existing.audio_path);
  return { id: poiId, routeId, deleted: true };
}

export function softDeleteRoute(db, id) {
  const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
  if (!route) throw new HttpError(404, 'Aktive Strecke nicht gefunden.');

  const moved = softDeleteRouteDirectory(id);
  try {
    transaction(db, () => {
      db.prepare(`UPDATE routes SET status='deleted', deleted_at=CURRENT_TIMESTAMP, trash_path=?,
        gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
        .run(moved.targetPrefix, moved.sourcePrefix, moved.targetPrefix, id);
      rewriteMediaPaths(db, id, moved.sourcePrefix, moved.targetPrefix);
    });
  } catch (error) {
    fs.renameSync(moved.destination, routeDirectory(id));
    throw error;
  }

  return rowToRoute(db.prepare('SELECT * FROM routes WHERE id = ?').get(id), true);
}

export function restoreRoute(db, id) {
  const route = db.prepare("SELECT trash_path FROM routes WHERE id = ? AND status = 'deleted'").get(id);
  if (!route) throw new HttpError(404, 'Gelöschte Strecke nicht gefunden.');

  const moved = restoreRouteDirectory(id, route.trash_path);
  try {
    transaction(db, () => {
      db.prepare(`UPDATE routes SET status='active', deleted_at=NULL, trash_path=NULL,
        gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
        .run(moved.sourcePrefix, moved.targetPrefix, id);
      rewriteMediaPaths(db, id, moved.sourcePrefix, moved.targetPrefix);
    });
  } catch (error) {
    fs.renameSync(moved.destination, moved.source);
    throw error;
  }

  return getRoute(db, id);
}
