import http from "http";

import cors from "cors";
import express from "express";
import helmet from "helmet";

import { env } from "./config/env.js";
import { db, isDatabaseBusyError, withDatabaseAdvisoryLock } from "./lib/db.js";
import { HttpError } from "./lib/http-error.js";
import { alertsRouter } from "./modules/alerts/alerts.routes.js";
import { authRouter } from "./modules/auth/auth.routes.js";
import { ensureAdminUser } from "./modules/auth/auth.service.js";
import { devicesRouter } from "./modules/devices/devices.routes.js";
import { createTelemetryRouter } from "./modules/telemetry/telemetry.routes.js";
import { TelemetryService } from "./modules/telemetry/telemetry.service.js";
import { pruneExpiredPositionRawPayloads } from "./modules/telemetry/position-storage.service.js";
import { geocodingRouter } from "./modules/geocoding/geocoding.routes.js";
import { settingsRouter } from "./modules/settings/settings.routes.js";
import { ensureSettingsStorage } from "./modules/settings/settings.service.js";
import { usersRouter } from "./modules/users/users.routes.js";
import {
  backfillVehicleTrips,
  enrichVehicleTripAddresses,
  ensureVehicleTripsStorage,
} from "./modules/vehicles/trips.service.js";
import { vehiclesRouter } from "./modules/vehicles/vehicles.routes.js";
import { createTrackerTcpServer } from "./protocols/trackerking/tcpHandler.js";
import { createRealtimeServer } from "./realtime/socket.js";

const start = async () => {
  await db.query("select 1");
  await ensureSettingsStorage();
  await ensureVehicleTripsStorage();
  await ensureAdminUser();

  const app = express();
  const httpServer = http.createServer(app);
  const io = createRealtimeServer(httpServer);
  const telemetryService = new TelemetryService(io);
  const tcpServer = createTrackerTcpServer(telemetryService);
  let tripAddressEnrichmentPromise: Promise<void> | null = null;
  let positionStorageMaintenancePromise: Promise<void> | null = null;

  const runPositionStorageMaintenance = () => {
    if (positionStorageMaintenancePromise) {
      return positionStorageMaintenancePromise;
    }

    positionStorageMaintenancePromise = withDatabaseAdvisoryLock(
      "position-storage-maintenance",
      async () => {
        const updatedCount = await pruneExpiredPositionRawPayloads();
        if (updatedCount > 0) {
          console.log("expired position raw payloads pruned", { updatedCount });
        }
      },
    ).then((result) => {
      if (!result.acquired) {
        console.log("position storage maintenance skipped; another instance is running");
      }
    }).catch((error) => {
      console.error("failed to maintain position storage", error);
    }).finally(() => {
      positionStorageMaintenancePromise = null;
    });

    return positionStorageMaintenancePromise;
  };

  const runTripAddressEnrichment = () => {
    if (tripAddressEnrichmentPromise) {
      return tripAddressEnrichmentPromise;
    }

    tripAddressEnrichmentPromise = (async () => {
      const result = await withDatabaseAdvisoryLock(
        "trip-address-enrichment",
        () => enrichVehicleTripAddresses(100),
      );

      if (!result.acquired) {
        console.log("trip address enrichment skipped; another instance is running");
      }
    })().catch((error) => {
      console.error("failed to enrich vehicle trip addresses", error);
    }).finally(() => {
      tripAddressEnrichmentPromise = null;
    });

    return tripAddressEnrichmentPromise;
  };

  app.set("trust proxy", 1);
  app.use(
    cors({
      origin: [env.APP_BASE_URL],
      credentials: true,
    }),
  );
  app.use(helmet());
  app.use(express.json({ limit: "1mb" }));

  app.get("/health", (_request, response) => {
    response.json({
      ok: true,
      apiBaseUrl: env.API_BASE_URL,
      tcpPort: env.TCP_PORT,
      timestamp: new Date().toISOString(),
    });
  });

  app.use("/auth", authRouter);
  app.use("/alerts", alertsRouter);
  app.use("/users", usersRouter);
  app.use("/vehicles", vehiclesRouter);
  app.use("/devices", devicesRouter);
  app.use("/geocoding", geocodingRouter);
  app.use("/settings", settingsRouter);
  app.use("/telemetry", createTelemetryRouter(telemetryService));

  app.use((error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
    if (error instanceof HttpError) {
      return response.status(error.statusCode).json({ message: error.message });
    }

    if (isDatabaseBusyError(error)) {
      console.error("database query cancelled by safety timeout", error);
      return response.status(503).json({ message: "Database is temporarily busy" });
    }

    console.error(error);
    return response.status(500).json({ message: "Internal server error" });
  });

  httpServer.listen(env.API_PORT, () => {
    console.log(`api listening on ${env.API_PORT}`);
  });

  tcpServer.listen(env.TCP_PORT, () => {
    console.log(`tcp listener on ${env.TCP_PORT}`);
  });

  const positionStorageInitialTimeout = setTimeout(() => {
    void runPositionStorageMaintenance();
  }, 60_000);
  positionStorageInitialTimeout.unref();

  const positionStorageMaintenanceInterval = setInterval(() => {
    void runPositionStorageMaintenance();
  }, 24 * 60 * 60 * 1000);
  positionStorageMaintenanceInterval.unref();

  if (env.TRIP_BACKFILL_ON_START) {
    setTimeout(() => {
      void withDatabaseAdvisoryLock(
        "vehicle-trip-backfill",
        () => backfillVehicleTrips(env.TRIP_BACKFILL_DAYS),
      ).then((result) => {
        if (!result.acquired) {
          console.log("vehicle trip backfill skipped; another instance is running");
        }
      }).catch((error) => {
        console.error("failed to backfill vehicle trips", error);
      });
    }, 2_000);
  }

  setTimeout(() => {
    void runTripAddressEnrichment();
  }, 4_000);

  setInterval(() => {
    void runTripAddressEnrichment();
  }, 15 * 60 * 1000);

  const shutdown = async () => {
    tcpServer.close();
    io.close();
    httpServer.close();
    await db.end();
    process.exit(0);
  };

  process.on("SIGINT", () => void shutdown());
  process.on("SIGTERM", () => void shutdown());
};

void start().catch((error) => {
  console.error("failed to start api", error);
  process.exit(1);
});
