const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
const express = require('express');
const cors = require('cors');

const loadEnvFiles = (filePaths) => {
  const mergedFileEnv = {};
  for (const filePath of filePaths) {
    if (!fs.existsSync(filePath)) continue;
    Object.assign(mergedFileEnv, dotenv.parse(fs.readFileSync(filePath)));
  }

  for (const [key, value] of Object.entries(mergedFileEnv)) {
    if (process.env[key] === undefined) {
      process.env[key] = value;
    }
  }
};

loadEnvFiles([
  path.join(__dirname, '..', '.env'),
  path.join(__dirname, '.env'),
  path.join(__dirname, '..', '.env.local'),
  path.join(__dirname, '.env.local'),
]);

const { closeDatabase, getDefaultDbPath, openDatabase, get } = require('./lib/postgres');
const { ensureAuthSchema, signUp: authSignUp, login: authLogin, issueToken, verifyJwt } = require('./lib/auth');
const {
  PlantImportValidationError,
  ensurePlantSchema,
  getPlantDiagnostics,
  getPlants,
  rebuildPlantsCatalog,
} = require('./lib/plants');
const {
  chargeKey,
  consumeCreditsWithIdempotency,
  endpointKey,
  ensureBillingSchema,
  getAccountSnapshot,
  getBillingSummary,
  getEndpointResponse,
  isInsufficientCreditsError,
  simulatePurchase,
  simulateWebhook,
  syncRevenueCatCustomerInfo,
  syncRevenueCatWebhookEvent,
  storeEndpointResponse,
} = require('./lib/billing');
const {
  analyzePlantHealth,
  getHealthModel,
  getScanModel,
  identifyPlant,
  isConfigured: isOpenAiConfigured,
} = require('./lib/openai');
const { applyCatalogGrounding, normalizeText } = require('./lib/scanGrounding');
const { ensureStorageBucketWithRetry, uploadImage, isStorageConfigured } = require('./lib/storage');

const app = express();
const port = Number(process.env.PORT || 3000);
const plantsPublicDir = path.join(__dirname, 'public', 'plants');

const SCAN_PRIMARY_COST = 1;
const SCAN_REVIEW_COST = 1;
const SEMANTIC_SEARCH_COST = 2;
const HEALTH_CHECK_COST = 2;
const LOW_CONFIDENCE_REVIEW_THRESHOLD = 0.8;

const DEFAULT_BOOTSTRAP_PLANTS = [
  {
    id: '1',
    name: 'Monstera Deliciosa',
    botanicalName: 'Monstera deliciosa',
    imageUri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg',
    description: 'A popular houseplant with large, holey leaves.',
    categories: ['easy', 'large', 'air_purifier'],
    confidence: 1,
    careInfo: {
      waterIntervalDays: 7,
      temp: '18-27C',
      light: 'Indirect bright light',
    },
  },
  {
    id: '2',
    name: 'Snake Plant',
    botanicalName: 'Sansevieria trifasciata',
    imageUri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg/330px-Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg',
    description: 'A hardy indoor plant known for its upright, sword-like leaves.',
    categories: ['succulent', 'easy', 'low_light', 'air_purifier'],
    confidence: 1,
    careInfo: {
      waterIntervalDays: 21,
      temp: '15-30C',
      light: 'Low to full light',
    },
  },
];

const FULL_BOOTSTRAP_CATALOG_CANDIDATES = [
  path.join(__dirname, 'data', 'plants_dump_utf8.json'),
  path.join(__dirname, '..', 'plants_dump_utf8.json'),
];
const FULL_BOOTSTRAP_MANIFEST_CANDIDATES = [
  path.join(__dirname, 'public', 'plants', 'manifest.json'),
];

let db;

const parseBoolean = (value, fallbackValue) => {
  if (typeof value !== 'string') return fallbackValue;
  const normalized = value.trim().toLowerCase();
  if (normalized === 'true' || normalized === '1') return true;
  if (normalized === 'false' || normalized === '0') return false;
  return fallbackValue;
};

const hashString = (value) => {
  let hash = 0;
  for (let i = 0; i < value.length; i += 1) {
    hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
  }
  return Math.abs(hash);
};

const clamp = (value, min, max) => {
  return Math.min(max, Math.max(min, value));
};

const nowIso = () => new Date().toISOString();

const hasImportAdminKey = Boolean(process.env.PLANT_IMPORT_ADMIN_KEY);
const isAuthorizedImport = (request) => {
  if (!hasImportAdminKey) return true;
  const provided = request.header('x-admin-key');
  return provided === process.env.PLANT_IMPORT_ADMIN_KEY;
};

const normalizeLanguage = (value) => {
  return value === 'de' || value === 'en' || value === 'es' ? value : 'en';
};

const resolveUserId = (request) => {
  // 1. Bearer JWT (preferred — server-side auth)
  const authHeader = request.header('authorization');
  if (authHeader && authHeader.startsWith('Bearer ')) {
    const token = authHeader.slice(7);
    if (token === 'guest') return 'guest';
    const payload = verifyJwt(token);
    if (payload && payload.sub) return String(payload.sub);
  }
  // 2. Legacy X-User-Id header (kept for backward compat)
  const headerUserId = request.header('x-user-id');
  if (typeof headerUserId === 'string' && headerUserId.trim()) return headerUserId.trim();
  const bodyUserId = typeof request.body?.userId === 'string' ? request.body.userId.trim() : '';
  if (bodyUserId) return bodyUserId;
  return '';
};

const resolveIdempotencyKey = (request) => {
  const header = request.header('idempotency-key');
  if (typeof header === 'string' && header.trim()) return header.trim();
  return '';
};

const toPlantResult = (entry, confidence) => {
  return {
    name: entry.name,
    botanicalName: entry.botanicalName,
    confidence: clamp(confidence, 0.05, 0.99),
    description: entry.description || `${entry.name} identified from the plant catalog.`,
    careInfo: {
      waterIntervalDays: Math.max(1, Number(entry.careInfo?.waterIntervalDays) || 7),
      light: entry.careInfo?.light || 'Unknown',
      temp: entry.careInfo?.temp || 'Unknown',
    },
  };
};

const pickCatalogFallback = (entries, imageUri, preferHighConfidence = false, { silent = false } = {}) => {
  if (!Array.isArray(entries) || entries.length === 0) return null;
  const baseHash = hashString(`${imageUri || ''}|${entries.length}`);
  const index = baseHash % entries.length;
  // Low confidence so the user knows this is a hash-based guess, not a real identification
  const confidence = preferHighConfidence
    ? 0.22 + ((baseHash % 3) / 100)
    : 0.18 + ((baseHash % 7) / 100);
  if (!silent) {
    console.warn('Using hash-based catalog fallback — OpenAI is unavailable or returned null.', {
      plant: entries[index]?.name,
      confidence,
      imageHint: (imageUri || '').slice(0, 80),
    });
  }
  return toPlantResult(entries[index], confidence);
};

const toImportErrorPayload = (error) => {
  if (error instanceof PlantImportValidationError) {
    return {
      status: 422,
      body: {
        code: 'IMPORT_VALIDATION_ERROR',
        message: error.message,
        details: error.details || [],
      },
    };
  }

  return {
    status: 500,
    body: {
      code: 'INTERNAL_ERROR',
      message: error instanceof Error ? error.message : String(error),
    },
  };
};

const toApiErrorPayload = (error) => {
  if (error && typeof error === 'object' && error.code === 'BAD_REQUEST') {
    return {
      status: 400,
      body: { code: 'BAD_REQUEST', message: error.message || 'Invalid request.' },
    };
  }

  if (error && typeof error === 'object' && error.code === 'UNAUTHORIZED') {
    return {
      status: 401,
      body: { code: 'UNAUTHORIZED', message: error.message || 'Unauthorized.' },
    };
  }

  if (isInsufficientCreditsError(error)) {
    return {
      status: 402,
      body: {
        code: 'INSUFFICIENT_CREDITS',
        message: error.message || 'Insufficient credits.',
        details: error.metadata || undefined,
      },
    };
  }

  if (error && typeof error === 'object' && error.code === 'PROVIDER_ERROR') {
    return {
      status: 502,
      body: { code: 'PROVIDER_ERROR', message: error.message || 'Provider request failed.' },
    };
  }

  if (error && typeof error === 'object' && error.code === 'TIMEOUT') {
    return {
      status: 504,
      body: { code: 'TIMEOUT', message: error.message || 'Provider timed out.' },
    };
  }

  return {
    status: 500,
    body: {
      code: 'PROVIDER_ERROR',
      message: error instanceof Error ? error.message : String(error),
    },
  };
};

const ensureRequestAuth = (request) => {
  const userId = resolveUserId(request);
  if (!userId) {
    const error = new Error('Missing X-User-Id header.');
    error.code = 'UNAUTHORIZED';
    throw error;
  }
  return userId;
};

const isGuest = (userId) => userId === 'guest';

const ensureNonEmptyString = (value, fieldName) => {
  if (typeof value === 'string' && value.trim()) return value.trim();
  const error = new Error(`${fieldName} is required.`);
  error.code = 'BAD_REQUEST';
  throw error;
};

const readJsonFromCandidates = (filePaths) => {
  for (const filePath of filePaths) {
    if (!fs.existsSync(filePath)) continue;

    try {
      const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
      return {
        parsed: JSON.parse(raw),
        sourcePath: filePath,
      };
    } catch (error) {
      console.warn('Failed to parse bootstrap JSON file.', {
        filePath,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  return null;
};

const buildEntriesFromManifest = (manifest) => {
  const items = Array.isArray(manifest?.items) ? manifest.items : [];
  return items
    .filter((item) => item && typeof item.name === 'string' && typeof item.botanicalName === 'string')
    .map((item) => ({
      id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : `${item.botanicalName}`.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
      name: item.name.trim(),
      botanicalName: item.botanicalName.trim(),
      imageUri: typeof item.localImageUri === 'string' && item.localImageUri.trim()
        ? item.localImageUri.trim()
        : (typeof item.sourceUri === 'string' ? item.sourceUri.trim() : ''),
      imageStatus: item.status === 'missing' ? 'missing' : 'ok',
      description: '',
      categories: [],
      confidence: 1,
      careInfo: {
        waterIntervalDays: 7,
        light: 'Unknown',
        temp: 'Unknown',
      },
    }))
    .filter((entry) => entry.imageUri);
};

const mergeBootstrapEntries = (primaryEntries, secondaryEntries) => {
  const mergedByBotanical = new Map();

  primaryEntries.forEach((entry) => {
    const botanicalKey = typeof entry?.botanicalName === 'string'
      ? entry.botanicalName.trim().toLowerCase()
      : '';
    if (!botanicalKey || mergedByBotanical.has(botanicalKey)) return;
    mergedByBotanical.set(botanicalKey, { ...entry });
  });

  secondaryEntries.forEach((entry) => {
    const botanicalKey = typeof entry?.botanicalName === 'string'
      ? entry.botanicalName.trim().toLowerCase()
      : '';
    if (!botanicalKey) return;

    const existing = mergedByBotanical.get(botanicalKey);
    if (!existing) {
      mergedByBotanical.set(botanicalKey, { ...entry });
      return;
    }

    const shouldPreferLocalImage = typeof entry.imageUri === 'string' && entry.imageUri.startsWith('/plants/');
    mergedByBotanical.set(botanicalKey, {
      ...existing,
      imageUri: shouldPreferLocalImage ? entry.imageUri : existing.imageUri,
      imageStatus: shouldPreferLocalImage ? entry.imageStatus || existing.imageStatus : existing.imageStatus,
      id: existing.id || entry.id,
      name: existing.name || entry.name,
      botanicalName: existing.botanicalName || entry.botanicalName,
    });
  });

  return Array.from(mergedByBotanical.values());
};

const loadFullBootstrapCatalog = () => {
  const catalogDump = readJsonFromCandidates(FULL_BOOTSTRAP_CATALOG_CANDIDATES);
  const manifestDump = readJsonFromCandidates(FULL_BOOTSTRAP_MANIFEST_CANDIDATES);

  const catalogEntries = Array.isArray(catalogDump?.parsed) ? catalogDump.parsed : [];
  const manifestEntries = manifestDump ? buildEntriesFromManifest(manifestDump.parsed) : [];
  const mergedEntries = mergeBootstrapEntries(catalogEntries, manifestEntries);

  if (mergedEntries.length === 0) return null;

  return {
    entries: mergedEntries,
    sourcePath: [catalogDump?.sourcePath, manifestDump?.sourcePath].filter(Boolean).join(', '),
  };
};

const isMinimalBootstrapCatalog = (entries) => {
  if (!Array.isArray(entries) || entries.length !== DEFAULT_BOOTSTRAP_PLANTS.length) {
    return false;
  }

  const botanicalNames = new Set(
    entries
      .map((entry) => (typeof entry?.botanicalName === 'string' ? entry.botanicalName.trim().toLowerCase() : ''))
      .filter(Boolean),
  );

  return DEFAULT_BOOTSTRAP_PLANTS.every((entry) => botanicalNames.has(entry.botanicalName.trim().toLowerCase()));
};

const seedBootstrapCatalogIfNeeded = async () => {
  const fullCatalog = loadFullBootstrapCatalog();
  const diagnostics = await getPlantDiagnostics(db);

  if (diagnostics.totalCount > 0) {
    if (fullCatalog && diagnostics.totalCount === DEFAULT_BOOTSTRAP_PLANTS.length) {
      const existingEntries = await getPlants(db, { limit: DEFAULT_BOOTSTRAP_PLANTS.length + 1 });
      if (isMinimalBootstrapCatalog(existingEntries) && fullCatalog.entries.length > existingEntries.length) {
        await rebuildPlantsCatalog(db, fullCatalog.entries, {
          source: 'bootstrap_upgrade_from_minimal_catalog',
          preserveExistingIds: false,
          enforceUniqueImages: false,
        });
        console.log(`Upgraded minimal bootstrap catalog to full catalog (${fullCatalog.entries.length} entries).`);
      }
    }
    return;
  }

  if (fullCatalog) {
    await rebuildPlantsCatalog(db, fullCatalog.entries, {
      source: 'bootstrap_full_catalog',
      preserveExistingIds: false,
      enforceUniqueImages: false,
    });
    console.log(`Bootstrapped full plant catalog from ${fullCatalog.sourcePath} (${fullCatalog.entries.length} entries).`);
    return;
  }

  await rebuildPlantsCatalog(db, DEFAULT_BOOTSTRAP_PLANTS, {
    source: 'bootstrap_minimal_catalog',
    preserveExistingIds: false,
    enforceUniqueImages: false,
  });
  console.warn('Full bootstrap catalog was not found. Seeded minimal fallback catalog with 2 entries.');
};

app.use(cors());
app.use('/plants', express.static(plantsPublicDir));

const revenueCatWebhookSecret = (process.env.REVENUECAT_WEBHOOK_SECRET || '').trim();

const isAuthorizedRevenueCatWebhook = (request) => {
  if (!revenueCatWebhookSecret) return true;
  const headerValue = request.header('authorization') || request.header('Authorization') || '';
  const normalized = String(headerValue).trim();
  return normalized === revenueCatWebhookSecret || normalized === `Bearer ${revenueCatWebhookSecret}`;
};

app.post('/api/revenuecat/webhook', express.json({ limit: '1mb' }), async (request, response) => {
  try {
    if (!isAuthorizedRevenueCatWebhook(request)) {
      return response.status(401).json({ code: 'UNAUTHORIZED', message: 'Invalid RevenueCat webhook secret.' });
    }
    const eventPayload = request.body?.event || request.body;
    const result = await syncRevenueCatWebhookEvent(db, eventPayload);
    response.status(200).json({ received: true, syncedAt: result.syncedAt });
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.use(express.json({ limit: '10mb' }));

app.get('/', (_request, response) => {
  response.status(200).json({
    service: 'greenlns-api',
    status: 'ok',
    endpoints: [
      'GET /health',
      'GET /api/plants',
      'POST /api/plants/rebuild',
      'POST /auth/signup',
      'POST /auth/login',
      'GET /v1/billing/summary',
      'POST /v1/billing/sync-revenuecat',
      'POST /v1/scan',
      'POST /v1/search/semantic',
      'POST /v1/health-check',
      'POST /v1/billing/simulate-purchase',
      'POST /v1/billing/simulate-webhook',
      'POST /v1/upload/image',
      'POST /api/revenuecat/webhook',
    ],
  });
});

const getDatabaseHealthTarget = () => {
  const raw = getDefaultDbPath();
  if (!raw) return '';

  try {
    const parsed = new URL(raw);
    const databaseName = parsed.pathname.replace(/^\//, '');
    return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}/${databaseName}`;
  } catch {
    return 'configured';
  }
};

app.get('/health', (_request, response) => {
  response.status(200).json({
    ok: true,
    uptimeSec: Math.round(process.uptime()),
    timestamp: new Date().toISOString(),
    openAiConfigured: isOpenAiConfigured(),
    dbReady: Boolean(db),
    dbPath: getDatabaseHealthTarget(),
    scanModel: getScanModel(),
    healthModel: getHealthModel(),
  });
});

app.get('/api/plants', async (request, response) => {
  try {
    const query = typeof request.query.q === 'string' ? request.query.q : '';
    const category = typeof request.query.category === 'string' ? request.query.category : '';
    const limit = request.query.limit;
    const results = await getPlants(db, {
      query,
      category,
      limit: typeof limit === 'string' ? Number(limit) : undefined,
    });
    response.json(results);
  } catch (error) {
    const payload = toImportErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.get('/api/plants/diagnostics', async (_request, response) => {
  try {
    const diagnostics = await getPlantDiagnostics(db);
    response.json(diagnostics);
  } catch (error) {
    const payload = toImportErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/api/plants/rebuild', async (request, response) => {
  if (!isAuthorizedImport(request)) {
    response.status(401).json({
      code: 'UNAUTHORIZED',
      message: 'Invalid or missing x-admin-key.',
    });
    return;
  }

  const payloadEntries = Array.isArray(request.body)
    ? request.body
    : request.body?.entries;
  const source = typeof request.body?.source === 'string' && request.body.source.trim()
    ? request.body.source.trim()
    : 'api_rebuild';
  const preserveExistingIds = parseBoolean(request.body?.preserveExistingIds, true);
  const enforceUniqueImages = parseBoolean(request.body?.enforceUniqueImages, true);

  try {
    const summary = await rebuildPlantsCatalog(db, payloadEntries, {
      source,
      preserveExistingIds,
      enforceUniqueImages,
    });
    response.status(200).json(summary);
  } catch (error) {
    const payload = toImportErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.get('/v1/billing/summary', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    if (userId !== 'guest') {
      const userExists = await get(db, 'SELECT id FROM auth_users WHERE id = $1', [userId]);
      if (!userExists) {
        return response.status(401).json({ code: 'UNAUTHORIZED', message: 'User not found.' });
      }
    }
    const summary = await getBillingSummary(db, userId);
    response.status(200).json(summary);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/billing/sync-revenuecat', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    if (userId === 'guest') {
      return response.status(400).json({ code: 'BAD_REQUEST', message: 'Guest users cannot sync RevenueCat state.' });
    }
    const customerInfo = request.body?.customerInfo;
    const source = typeof request.body?.source === 'string' ? request.body.source : undefined;
    if (!customerInfo || typeof customerInfo !== 'object' || !customerInfo.entitlements) {
      return response.status(400).json({ code: 'BAD_REQUEST', message: 'customerInfo is required.' });
    }
    const payload = await syncRevenueCatCustomerInfo(db, userId, customerInfo, { source });
    response.status(200).json(payload);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/scan', async (request, response) => {
  let userId = 'unknown';
  try {
    userId = ensureRequestAuth(request);
    const idempotencyKey = ensureNonEmptyString(resolveIdempotencyKey(request), 'Idempotency-Key header');
    const imageUri = ensureNonEmptyString(request.body?.imageUri, 'imageUri');
    const language = normalizeLanguage(request.body?.language);
    const endpointId = endpointKey('scan', userId, idempotencyKey);

    const cached = await getEndpointResponse(db, endpointId);
    if (cached) {
      response.status(200).json(cached);
      return;
    }

    let creditsCharged = 0;
    const modelPath = [];
    let modelUsed = null;
    let modelFallbackCount = 0;

    if (!isGuest(userId)) {
      creditsCharged += await consumeCreditsWithIdempotency(
        db,
        userId,
        chargeKey('scan-primary', userId, idempotencyKey),
        SCAN_PRIMARY_COST,
      );
    }

    const accountSnapshot = await getAccountSnapshot(db, userId);
    const scanPlan = accountSnapshot.plan === 'pro' ? 'pro' : 'free';
    const catalogEntries = await getPlants(db, { limit: 500 });
    let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
    let usedOpenAi = false;

    if (isOpenAiConfigured()) {
      console.log(`Starting OpenAI identification for user ${userId} using model ${getScanModel(scanPlan)} (plan: ${scanPlan})`);
      const openAiPrimary = await identifyPlant({
        imageUri,
        language,
        mode: 'primary',
        plan: scanPlan,
      });
      modelFallbackCount = Math.max(
        modelFallbackCount,
        Math.max((openAiPrimary?.attemptedModels?.length || 0) - 1, 0),
      );
      if (openAiPrimary?.result) {
        console.log(`OpenAI primary identification successful for user ${userId}: ${openAiPrimary.result.name} (${openAiPrimary.result.confidence}) using ${openAiPrimary.modelUsed}`);
        const grounded = applyCatalogGrounding(openAiPrimary.result, catalogEntries, language);
        result = grounded.result;
        if (!grounded.grounded) result = { ...result, confidence: clamp(Math.max(result.confidence || 0.6, 0.72), 0.05, 0.99) };
        usedOpenAi = true;
        modelUsed = openAiPrimary.modelUsed || modelUsed;
        modelPath.push('openai-primary');
        if (grounded.grounded) modelPath.push('catalog-grounded-primary');
      } else {
        console.warn(`OpenAI primary identification returned null for user ${userId} — using catalog fallback.`, {
          attemptedModels: openAiPrimary?.attemptedModels,
          plant: result?.name,
        });
        modelPath.push('openai-primary-failed');
        modelPath.push('catalog-primary-fallback');
      }
    } else {
      console.log(`OpenAI not configured, using catalog fallback for user ${userId}`);
      modelPath.push('openai-not-configured');
      modelPath.push('catalog-primary-fallback');
    }

    if (!result) {
      const error = new Error('Plant catalog is empty. Unable to produce identification fallback.');
      error.code = 'PROVIDER_ERROR';
      throw error;
    }

    const shouldReview = result.confidence < LOW_CONFIDENCE_REVIEW_THRESHOLD;
    if (shouldReview && accountSnapshot.plan === 'pro') {
      console.log(`Starting AI review for user ${userId} (confidence ${result.confidence} < ${LOW_CONFIDENCE_REVIEW_THRESHOLD})`);
      try {
        if (!isGuest(userId)) {
          creditsCharged += await consumeCreditsWithIdempotency(
            db,
            userId,
            chargeKey('scan-review', userId, idempotencyKey),
            SCAN_REVIEW_COST,
          );
        }

        if (usedOpenAi) {
          const openAiReview = await identifyPlant({
            imageUri,
            language,
            mode: 'review',
            plan: scanPlan,
          });
          modelFallbackCount = Math.max(
            modelFallbackCount,
            Math.max((openAiReview?.attemptedModels?.length || 0) - 1, 0),
          );
          if (openAiReview?.result) {
            console.log(`OpenAI review identification successful for user ${userId}: ${openAiReview.result.name} (${openAiReview.result.confidence}) using ${openAiReview.modelUsed}`);
            const grounded = applyCatalogGrounding(openAiReview.result, catalogEntries, language);
            result = grounded.result;
            if (!grounded.grounded) result = { ...result, confidence: clamp(Math.max(result.confidence || 0.6, 0.72), 0.05, 0.99) };
            modelUsed = openAiReview.modelUsed || modelUsed;
            modelPath.push('openai-review');
            if (grounded.grounded) modelPath.push('catalog-grounded-review');
          } else {
            console.warn(`OpenAI review identification returned null for user ${userId}.`, {
              attemptedModels: openAiReview?.attemptedModels,
            });
            modelPath.push('openai-review-failed');
          }
        } else {
          const reviewFallback = pickCatalogFallback(catalogEntries, `${imageUri}|review`, true, { silent: true });
          if (reviewFallback) {
            result = reviewFallback;
          }
          modelPath.push('catalog-review-fallback');
        }
      } catch (error) {
        if (isInsufficientCreditsError(error)) {
          console.log(`Review skipped for user ${userId} due to insufficient credits`);
          modelPath.push('review-skipped-insufficient-credits');
        } else {
          throw error;
        }
      }
    } else if (shouldReview) {
      console.log(`Review skipped for user ${userId} (plan: ${accountSnapshot.plan})`);
      modelPath.push('review-skipped-free-plan');
    }

    const payload = {
      result,
      creditsCharged,
      modelPath,
      modelUsed,
      modelFallbackCount,
      billing: await getBillingSummary(db, userId),
    };

    await storeEndpointResponse(db, endpointId, payload);
    response.status(200).json(payload);
  } catch (error) {
    console.error(`Scan error for user ${userId}:`, error);
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/search/semantic', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    const idempotencyKey = ensureNonEmptyString(resolveIdempotencyKey(request), 'Idempotency-Key header');
    const query = typeof request.body?.query === 'string' ? request.body.query.trim() : '';
    const endpointId = endpointKey('semantic-search', userId, idempotencyKey);

    const cached = await getEndpointResponse(db, endpointId);
    if (cached) {
      response.status(200).json(cached);
      return;
    }

    if (!query) {
      const payload = {
        status: 'no_results',
        results: [],
        creditsCharged: 0,
        billing: await getBillingSummary(db, userId),
      };
      await storeEndpointResponse(db, endpointId, payload);
      response.status(200).json(payload);
      return;
    }

    const creditsCharged = await consumeCreditsWithIdempotency(
      db,
      userId,
      chargeKey('semantic-search', userId, idempotencyKey),
      SEMANTIC_SEARCH_COST,
    );

    const results = await getPlants(db, { query, limit: 18 });
    const payload = {
      status: results.length > 0 ? 'success' : 'no_results',
      results,
      creditsCharged,
      billing: await getBillingSummary(db, userId),
    };

    await storeEndpointResponse(db, endpointId, payload);
    response.status(200).json(payload);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/health-check', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    const idempotencyKey = ensureNonEmptyString(resolveIdempotencyKey(request), 'Idempotency-Key header');
    const imageUri = ensureNonEmptyString(request.body?.imageUri, 'imageUri');
    const language = normalizeLanguage(request.body?.language);
    const endpointId = endpointKey('health-check', userId, idempotencyKey);

    const cached = await getEndpointResponse(db, endpointId);
    if (cached) {
      response.status(200).json(cached);
      return;
    }

    if (!isOpenAiConfigured()) {
      const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');
      error.code = 'PROVIDER_ERROR';
      throw error;
    }

    const analysisResponse = await analyzePlantHealth({
      imageUri,
      language,
      plantContext: request.body?.plantContext,
    });
    const analysis = analysisResponse?.analysis;
    if (!analysis) {
      // All models in the chain failed (timeout, quota, network) — return a graceful
      // "unavailable" result instead of PROVIDER_ERROR so the user never sees an error alert.
      // Credits are NOT charged. Response is NOT cached so the user can retry.
      console.warn('Health check analysis was null — all models returned unusable output.', {
        attemptedModels: analysisResponse?.attemptedModels,
        modelUsed: analysisResponse?.modelUsed,
      });
      const unavailableIssue = language === 'de'
        ? 'Die KI-Analyse ist gerade nicht verfügbar. Bitte versuche es in einem Moment erneut.'
        : language === 'es'
          ? 'El análisis de IA no está disponible ahora. Inténtalo de nuevo en un momento.'
          : 'AI analysis is temporarily unavailable. Please try again in a moment.';
      const unavailableAction = language === 'de'
        ? 'Erneut scannen wenn die Verbindung stabil ist.'
        : language === 'es'
          ? 'Volver a escanear cuando la conexión sea estable.'
          : 'Try scanning again when your connection is stable.';
      const fallbackHealthCheck = {
        generatedAt: nowIso(),
        overallHealthScore: 50,
        status: 'watch',
        likelyIssues: [{
          title: language === 'de' ? 'Analyse nicht verfügbar' : language === 'es' ? 'Análisis no disponible' : 'Analysis unavailable',
          confidence: 0.1,
          details: unavailableIssue,
        }],
        actionsNow: [unavailableAction],
        plan7Days: [unavailableAction],
        creditsCharged: 0,
        imageUri,
      };
      const fallbackPayload = {
        healthCheck: fallbackHealthCheck,
        creditsCharged: 0,
        modelUsed: null,
        modelFallbackCount: Math.max((analysisResponse?.attemptedModels?.length || 0) - 1, 0),
        billing: await getBillingSummary(db, userId),
      };
      response.status(200).json(fallbackPayload);
      return;
    }

    let creditsCharged = 0;
    if (!isGuest(userId)) {
      creditsCharged = await consumeCreditsWithIdempotency(
        db,
        userId,
        chargeKey('health-check', userId, idempotencyKey),
        HEALTH_CHECK_COST,
      );
    }

    const healthCheck = {
      generatedAt: nowIso(),
      overallHealthScore: analysis.overallHealthScore,
      status: analysis.status,
      likelyIssues: analysis.likelyIssues,
      actionsNow: analysis.actionsNow,
      plan7Days: analysis.plan7Days,
      creditsCharged,
      imageUri,
    };

    const payload = {
      healthCheck,
      creditsCharged,
      modelUsed: analysisResponse?.modelUsed || null,
      modelFallbackCount: Math.max((analysisResponse?.attemptedModels?.length || 0) - 1, 0),
      billing: await getBillingSummary(db, userId),
    };

    await storeEndpointResponse(db, endpointId, payload);
    response.status(200).json(payload);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/billing/simulate-purchase', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    const idempotencyKey = ensureNonEmptyString(resolveIdempotencyKey(request), 'Idempotency-Key header');
    const productId = ensureNonEmptyString(request.body?.productId, 'productId');
    const payload = await simulatePurchase(db, userId, idempotencyKey, productId);
    response.status(200).json(payload);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

app.post('/v1/billing/simulate-webhook', async (request, response) => {
  try {
    const userId = ensureRequestAuth(request);
    const idempotencyKey = ensureNonEmptyString(resolveIdempotencyKey(request), 'Idempotency-Key header');
    const event = ensureNonEmptyString(request.body?.event, 'event');
    const payload = await simulateWebhook(db, userId, idempotencyKey, event, request.body?.payload || {});
    response.status(200).json(payload);
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

// ─── Image Upload ──────────────────────────────────────────────────────────

app.post('/v1/upload/image', async (request, response) => {
  try {
    ensureRequestAuth(request);

    if (!isStorageConfigured()) {
      return response.status(503).json({
        code: 'STORAGE_NOT_CONFIGURED',
        message: 'Image storage is not configured.',
      });
    }

    const { imageBase64, contentType = 'image/jpeg' } = request.body || {};
    if (!imageBase64 || typeof imageBase64 !== 'string') {
      return response.status(400).json({
        code: 'BAD_REQUEST',
        message: 'imageBase64 is required.',
      });
    }

    const { url } = await uploadImage(imageBase64, contentType);
    response.status(200).json({ url });
  } catch (error) {
    const payload = toApiErrorPayload(error);
    response.status(payload.status).json(payload.body);
  }
});

// ─── Auth endpoints ────────────────────────────────────────────────────────

app.post('/auth/signup', async (request, response) => {
  try {
    const { email, name, password } = request.body || {};
    if (!email || !name || !password) {
      return response.status(400).json({ code: 'BAD_REQUEST', message: 'email, name and password are required.' });
    }
    const user = await authSignUp(db, email, name, password);
    const token = issueToken(user.id, user.email, user.name);
    response.status(201).json({ userId: user.id, email: user.email, name: user.name, token });
  } catch (error) {
    const status = error.status || 500;
    response.status(status).json({ code: error.code || 'SERVER_ERROR', message: error.message });
  }
});

app.post('/auth/login', async (request, response) => {
  try {
    const { email, password } = request.body || {};
    if (!email || !password) {
      return response.status(400).json({ code: 'BAD_REQUEST', message: 'email and password are required.' });
    }
    const user = await authLogin(db, email, password);
    const token = issueToken(user.id, user.email, user.name);
    response.status(200).json({ userId: user.id, email: user.email, name: user.name, token });
  } catch (error) {
    const status = error.status || 500;
    response.status(status).json({ code: error.code || 'SERVER_ERROR', message: error.message });
  }
});

// ─── Startup ───────────────────────────────────────────────────────────────

const start = async () => {
  db = await openDatabase();
  await ensurePlantSchema(db);
  await ensureBillingSchema(db);
  await ensureAuthSchema(db);
  await seedBootstrapCatalogIfNeeded();
  if (isStorageConfigured()) {
    await ensureStorageBucketWithRetry().catch((err) => console.warn('MinIO bucket setup failed:', err.message));
  }

  const server = app.listen(port, () => {
    console.log(`GreenLens server listening at http://localhost:${port}`);
  });

  const gracefulShutdown = async () => {
    try {
      await closeDatabase(db);
    } catch (error) {
      console.error('Failed to close database', error);
    } finally {
      server.close(() => process.exit(0));
    }
  };

  process.on('SIGINT', gracefulShutdown);
  process.on('SIGTERM', gracefulShutdown);
};

start().catch((error) => {
  console.error('Failed to start GreenLens server', error);
  process.exit(1);
});
