import { Hono } from 'hono';
import { bodyLimit } from 'hono/body-limit';

import { albumMigrations, albumImajinPortMigrations } from '@/entities/album';
import { attachmentMigrations } from '@/entities/attachment';
import { calendarMigrations } from '@/entities/calendar';
import { calendarSendItemMigrations } from '@/entities/calendarSendItem';
import { contactMigrations } from '@/entities/contact';
import { conversationMigrations } from '@/entities/conversation';
import { deviceMigrations } from '@/entities/device';
import { eventMigrations } from '@/entities/event';
import { faceMigrations } from '@/entities/face';
import { identityMigrations } from '@/entities/identity';
import { mailSendItemMigrations } from '@/entities/mailSendItem';
import { messageMigrations } from '@/entities/message';
import { messageEmbeddingMigrations } from '@/entities/message-embedding';
import { noteMigrations } from '@/entities/note';
import { noteSendItemMigrations } from '@/entities/noteSendItem';
import { outreachMigrations } from '@/entities/outreach';
import { photoMigrations } from '@/entities/photo';
import { photoAlbumMigrations } from '@/entities/photo-album';
import { prospectMigrations } from '@/entities/prospect';
import { reminderMigrations } from '@/entities/reminder';
import { reminderSendItemMigrations } from '@/entities/reminderSendItem';
import { searchCacheMigrations } from '@/entities/search-cache';
import { sendQueueMigrations } from '@/entities/send-queue';
import { syncHistoryMigrations } from '@/entities/sync-history';
import { clientSurface } from '@/surfaces/client';
import { mySurface } from '@/surfaces/my';
import { adminSurface } from '@/surfaces/admin';
import { openDb, runMigrations, pgRun, getDb } from '@/shared/db';
import { logger } from '@/shared/logger';
import { startEmbeddingWorker } from '@/features/embedding/worker';
import { startThumbnailWorker } from '@/features/iphoto/thumbnail-worker';
import { startFaceWorker } from '@/features/iphoto/face-worker';
import { startClassifyWorker } from '@/features/iphoto/classify-worker';
import { startMatchWorker } from '@/features/identity/match-worker';
import { startCentroidWorker } from '@/features/identity/centroid-worker';
import { pingRedis, registerGracefulShutdown } from '@/shared/queues';

import { loadConfig } from './config';
import { deviceTokenAuth, serviceTokenAuth } from './middleware/auth';
import { corsMiddleware } from './middleware/cors';
import { errorHandler } from './middleware/errors';
import { rateLimitMiddleware } from './middleware/rate-limit';
import { ssoRequired } from './middleware/sso';

export async function createApp() {
  const config = loadConfig();
  await openDb('macsync');
  const db = getDb();

  await runMigrations(db, [
    ...deviceMigrations,
    ...contactMigrations,
    ...conversationMigrations,
    ...messageMigrations,
    ...messageEmbeddingMigrations,
    ...attachmentMigrations,
    ...albumMigrations,
    ...photoMigrations,
    // albumImajinPortMigrations FKs photos(id), so it must run after photoMigrations.
    ...albumImajinPortMigrations,
    ...photoAlbumMigrations,
    ...identityMigrations,
    ...faceMigrations,
    ...calendarMigrations,
    ...eventMigrations,
    ...noteMigrations,
    ...reminderMigrations,
    ...searchCacheMigrations,
    ...sendQueueMigrations,
    ...calendarSendItemMigrations,
    ...mailSendItemMigrations,
    ...noteSendItemMigrations,
    ...reminderSendItemMigrations,
    ...syncHistoryMigrations,
    ...prospectMigrations,
    ...outreachMigrations,
  ]);

  // Start incremental embedding worker — fire and forget; crashes reconnect automatically
  startEmbeddingWorker().catch((err) => {
    logger.error('failed to start embedding worker', { err: String(err) });
  });

  // Wire BullMQ graceful shutdown for media-processing workers.
  registerGracefulShutdown();
  startThumbnailWorker();
  startFaceWorker();
  startMatchWorker();
  startCentroidWorker();
  startClassifyWorker();

  const app = new Hono()
    .onError(errorHandler)
    .use('*', bodyLimit({ maxSize: 100 * 1024 * 1024 })) // 100MB for large iMessage batch uploads
    .get('/health', (c) => c.json({ ok: true }))
    .get('/health/deep', async (c) => {
      const checks: Record<string, string> = {};

      try {
        await pgRun(db, 'SELECT 1');
        checks['db'] = 'ok';
      } catch (err) {
        checks['db'] = `error: ${String(err)}`;
      }

      try {
        await pingRedis();
        checks['redis'] = 'ok';
      } catch (err) {
        checks['redis'] = `error: ${err instanceof Error ? err.message : String(err)}`;
      }

      if (config.STORAGE_BACKEND === 'minio' && config.MINIO_ENDPOINT) {
        try {
          const url = new URL('/minio/health/live', config.MINIO_ENDPOINT).toString();
          const res = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(3_000) });
          checks['minio'] = res.ok ? 'ok' : `error: status ${res.status}`;
        } catch (err) {
          checks['minio'] = `error: ${err instanceof Error ? err.message : String(err)}`;
        }
      }

      const ok = Object.values(checks).every((v) => v === 'ok');
      return c.json({ ok, checks }, ok ? 200 : 503);
    })
    .use('/client/*', rateLimitMiddleware('client'))
    .use('/client/devices/register', corsMiddleware('public-read'))
    .use('/client/*', (c, next) => {
      if (c.req.path === '/client/devices/register') return next();
      return corsMiddleware('same-origin')(c, next);
    })
    .use('/client/*', (c, next) => {
      if (c.req.path === '/client/devices/register') return next();
      // /client/devices/:id/status is polled before a token is issued — exempt from auth.
      if (/^\/client\/devices\/[^/]+\/status$/.test(c.req.path)) return next();
      // /client/sync/* is read-only aggregate — accept service token or device token.
      if (c.req.path.startsWith('/client/sync/')) return deviceTokenAuth(config.SERVICE_TOKEN)(c, next);
      return deviceTokenAuth(config.SERVICE_TOKEN)(c, next);
    })
    .route('/client', clientSurface)
    // TODO(merge-conflict): plum gates /my/* with `ssoRequired(SSO_VALIDATE_URL, SERVICE_TOKEN)`
    // (cookie/SSO session against an external validator); apricot gates it with
    // `serviceTokenAuth(SERVICE_TOKEN)` (single shared bearer token). These are
    // production auth models for the dashboard — pick one before deploy.
    // Currently using apricot's simpler service-token mode.
    .use('/my/*', corsMiddleware('same-origin'))
    .use('/my/*', rateLimitMiddleware('my'))
    .use('/my/*', serviceTokenAuth(config.SERVICE_TOKEN))
    .route('/my', mySurface)
    .use('/admin/*', corsMiddleware('same-origin'))
    .use('/admin/*', rateLimitMiddleware('admin'))
    .use('/admin/*', serviceTokenAuth(config.SERVICE_TOKEN))
    .route('/admin', adminSurface);

  return { app, config };
}
