import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import { URL } from 'node:url';
import { parseMultipartUpload } from './src/multipart.js';
import { sendFile, sendHtml, sendJson, sendText, withSecurityHeaders } from './src/http.js';
import { createStore } from './src/store.js';
import { validateImage } from './src/image.js';
import { normalizeToWebp } from './src/normalize.js';
import { seedDemoMemes } from './src/seed.js';
import { moderateImage } from './src/moderation.js';
import { createUploadLimiter } from './src/uploadLimits.js';
import { createIpRateLimiter } from './src/rateLimits.js';
import { feedJson, llmsTxt, manifest, noIndex, publicBaseUrl, renderIndex, renderNotFound, robotsTxt, sitemapXml } from './src/seo.js';

await loadDotEnv();

const PORT = Number.parseInt(process.env.PORT || '8080', 10);
const HOST = process.env.HOST || '0.0.0.0';
const DATA_DIR = process.env.DATA_DIR || './data';
const PAGE_SIZE_MAX = 48;
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
const API_READ_LIMIT_PER_MINUTE = 120;
const MEDIA_READ_LIMIT_PER_MINUTE = 600;
const SSE_HEARTBEAT_MS = 25_000;
const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
const REVALIDATING_CACHE_CONTROL = 'public, max-age=300, must-revalidate';
const FINGERPRINTED_ASSET = /\.[a-f0-9]{8,}\.[a-z0-9]+$/i;
const events = new Set();
const DISCOVERY_ROUTES = new Set([
  '/openapi.json',
  '/meme-api.skill.md',
  '/robots.txt',
  '/llms.txt',
  '/site.webmanifest',
  '/feed.json',
  '/sitemap.xml'
]);
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString('hex');
const indexTemplate = await fs.readFile('./public/index.html', 'utf8');
const notFoundTemplate = await fs.readFile('./public/404.html', 'utf8');
const openApiSpec = JSON.parse(await fs.readFile('./openapi.json', 'utf8'));
const memeApiSkillTemplate = await fs.readFile('./meme-api.skill.md', 'utf8');

const store = await createStore({ dataDir: DATA_DIR });
const uploadLimiter = await createUploadLimiter({ dataDir: DATA_DIR });
const apiReadLimiter = createIpRateLimiter({
  windowMs: 60_000,
  max: API_READ_LIMIT_PER_MINUTE,
  message: 'API read limit exceeded. Try again shortly.'
});
const mediaReadLimiter = createIpRateLimiter({
  windowMs: 60_000,
  max: MEDIA_READ_LIMIT_PER_MINUTE,
  message: 'Media read limit exceeded. Try again shortly.'
});
if (process.env.SEED_DEMO_MEMES !== 'false') {
  await seedDemoMemes(store);
}
if (!process.env.ADMIN_TOKEN) {
  console.log(`Admin review URL: /admin/${ADMIN_TOKEN}`);
}

const server = http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
    withSecurityHeaders(res, { contentSecurityPolicy: !isDiscoveryRoute(req, url) });
    const baseUrl = publicBaseUrl(req);
    const isPublicRead = isReadRequest(req);

    if (isPublicRead && url.pathname === '/') {
      const nonce = crypto.randomBytes(16).toString('base64');
      withSecurityHeaders(res, { scriptNonce: nonce });
      return sendHtml(res, 200, renderIndex({
        template: indexTemplate,
        baseUrl,
        nonce,
        approvedCount: store.count('approved')
      }));
    }

    if (isPublicRead && (url.pathname === '/lore' || url.pathname === '/lore/')) {
      const nonce = crypto.randomBytes(16).toString('base64');
      withSecurityHeaders(res, { scriptNonce: nonce });
      return sendHtml(res, 200, renderIndex({
        template: indexTemplate,
        baseUrl,
        nonce,
        approvedCount: store.count('approved'),
        lore: true
      }));
    }

    const memePageMatch = url.pathname.match(/^\/meme\/([a-f0-9]{64})\/?$/);
    if (isPublicRead && memePageMatch) {
      const meme = store.get(memePageMatch[1]);
      if (!meme || meme.status !== 'approved') {
        return sendNotFoundPage(res, { baseUrl, pathname: url.pathname });
      }
      const nonce = crypto.randomBytes(16).toString('base64');
      withSecurityHeaders(res, { scriptNonce: nonce });
      return sendHtml(res, 200, renderIndex({
        template: indexTemplate,
        baseUrl,
        nonce,
        approvedCount: store.count('approved'),
        meme: publicMeme(meme)
      }));
    }

    if (isPublicRead && url.pathname === '/openapi.json') {
      res.setHeader('Cache-Control', 'public, max-age=300');
      return sendJson(res, 200, openApiSpecFor(baseUrl));
    }

    if (isPublicRead && url.pathname === '/meme-api.skill.md') {
      res.setHeader('Cache-Control', 'public, max-age=300');
      return sendText(res, 200, renderMemeApiSkill(baseUrl), 'text/markdown; charset=utf-8');
    }

    if (isPublicRead && url.pathname === '/robots.txt') {
      return sendText(res, 200, robotsTxt(baseUrl));
    }

    if (isPublicRead && url.pathname === '/llms.txt') {
      return sendText(res, 200, llmsTxt(baseUrl));
    }

    if (isPublicRead && url.pathname === '/site.webmanifest') {
      return sendJson(res, 200, manifest(baseUrl));
    }

    if (isPublicRead && url.pathname === '/favicon.ico') {
      return sendFile(res, './public/assets/favicon/favicon.ico');
    }

    if (isPublicRead && url.pathname === '/feed.json') {
      return sendJson(res, 200, feedJson(baseUrl, store.listForReview({ status: 'approved' }).memes));
    }

    if (isPublicRead && url.pathname === '/sitemap.xml') {
      const body = sitemapXml(baseUrl, store.listForReview({ status: 'approved' }).memes);
      res.writeHead(200, {
        'Content-Type': 'application/xml; charset=utf-8',
        'Content-Length': String(Buffer.byteLength(body))
      });
      return res.end(body);
    }

    const adminPageMatch = url.pathname.match(/^\/admin\/([A-Za-z0-9_-]{24,128})$/);
    if (req.method === 'GET' && adminPageMatch) {
      if (!isAdminToken(adminPageMatch[1])) return sendText(res, 404, 'Not found');
      noIndex(res);
      return sendFile(res, './public/admin.html');
    }

    if (isPublicRead && url.pathname.startsWith('/assets/')) {
      const isVersioned = url.searchParams.has('v') || FINGERPRINTED_ASSET.test(url.pathname);
      res.setHeader('Cache-Control', isVersioned ? IMMUTABLE_CACHE_CONTROL : REVALIDATING_CACHE_CONTROL);
      return sendFile(res, `./public${url.pathname}`);
    }

    if (req.method === 'GET' && url.pathname === '/api/memes') {
      noIndex(res);
      const readQuota = checkReadLimit(req, res, apiReadLimiter);
      const page = positiveInt(url.searchParams.get('page'), 1);
      const pageSize = Math.min(positiveInt(url.searchParams.get('pageSize'), 12), PAGE_SIZE_MAX);
      return sendJson(res, 200, {
        ...store.list({ page, pageSize }),
        rateLimit: readQuota
      });
    }

    if (req.method === 'GET' && url.pathname === '/api/status') {
      noIndex(res);
      checkReadLimit(req, res, apiReadLimiter);
      return sendJson(res, 200, {
        ok: true,
        memeCount: store.count('approved'),
        liveClients: events.size,
        uptimeSeconds: Math.floor(process.uptime())
      });
    }

    if (req.method === 'GET' && url.pathname === '/api/admin/pending') {
      noIndex(res);
      if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
      return sendJson(res, 200, store.listForReview({ status: 'pending' }));
    }

    if (req.method === 'POST' && url.pathname === '/api/admin/approve') {
      noIndex(res);
      if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
      const body = await readJsonBody(req, 64 * 1024);
      const approved = await store.approve(safeIds(body.ids));
      return sendJson(res, 200, { approved });
    }

    if (req.method === 'POST' && url.pathname === '/api/admin/delete') {
      noIndex(res);
      if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
      const body = await readJsonBody(req, 64 * 1024);
      const deleted = await store.delete(safeIds(body.ids));
      return sendJson(res, 200, { deleted });
    }

    if (req.method === 'GET' && url.pathname === '/api/events') {
      noIndex(res);
      res.writeHead(200, {
        'Content-Type': 'text/event-stream; charset=utf-8',
        'Cache-Control': 'no-cache, no-transform',
        Connection: 'keep-alive',
        'X-Accel-Buffering': 'no'
      });
      res.write('retry: 5000\n');
      res.write('event: ready\ndata: {}\n\n');
      events.add(res);
      const heartbeat = setInterval(() => {
        res.write(': keep-alive\n\n');
      }, SSE_HEARTBEAT_MS);
      req.on('close', () => {
        clearInterval(heartbeat);
        events.delete(res);
      });
      return;
    }

    if (req.method === 'POST' && url.pathname === '/api/memes') {
      noIndex(res);
      const { meme, quota } = await addMeme(req);
      return sendJson(res, meme.status === 'approved' ? 201 : 202, {
        meme,
        quota,
        moderationScore: meme.moderationScore,
        moderationReason: meme.moderationReason,
        message: meme.status === 'approved' ? 'Upload approved.' : 'Upload queued for admin review.'
      });
    }

    const apiMemeMatch = url.pathname.match(/^\/api\/memes\/([a-f0-9]{64})$/);
    if (req.method === 'GET' && apiMemeMatch) {
      noIndex(res);
      const readQuota = checkReadLimit(req, res, apiReadLimiter);
      const meme = store.get(apiMemeMatch[1]);
      if (!meme || meme.status !== 'approved') return sendJson(res, 404, { error: 'Not found' });
      return sendJson(res, 200, { meme: publicMeme(meme), rateLimit: readQuota });
    }

    const viewMatch = url.pathname.match(/^\/api\/memes\/([a-f0-9]{64})\/view$/);
    if (req.method === 'POST' && viewMatch) {
      if (store.get(viewMatch[1])?.status !== 'approved') return sendText(res, 404, 'Not found');
      const meme = await store.incrementMetric(viewMatch[1], 'viewCount');
      if (!meme) return sendText(res, 404, 'Not found');
      broadcastMetric(meme);
      return sendJson(res, 200, { meme });
    }

    const mediaMatch = url.pathname.match(/^\/media\/([a-f0-9]{64})$/);
    if (isPublicRead && mediaMatch) {
      checkReadLimit(req, res, mediaReadLimiter);
      const meme = store.get(mediaMatch[1]);
      if (!meme || meme.status !== 'approved') return sendText(res, 404, 'Not found');
      res.setHeader('Content-Type', meme.mime);
      res.setHeader('Content-Length', String(meme.byteSize));
      res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
      res.setHeader('X-Content-Type-Options', 'nosniff');
      return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
    }

    const adminMediaMatch = url.pathname.match(/^\/admin-media\/([A-Za-z0-9_-]{24,128})\/([a-f0-9]{64})$/);
    if (req.method === 'GET' && adminMediaMatch) {
      noIndex(res);
      if (!isAdminToken(adminMediaMatch[1])) return sendText(res, 404, 'Not found');
      const meme = store.get(adminMediaMatch[2]);
      if (!meme) return sendText(res, 404, 'Not found');
      res.setHeader('Content-Type', meme.mime);
      res.setHeader('Content-Length', String(meme.byteSize));
      res.setHeader('Cache-Control', 'no-store');
      res.setHeader('X-Content-Type-Options', 'nosniff');
      return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
    }

    const downloadMatch = url.pathname.match(/^\/download\/([a-f0-9]{64})$/);
    if (req.method === 'GET' && downloadMatch) {
      noIndex(res);
      checkReadLimit(req, res, mediaReadLimiter);
      if (store.get(downloadMatch[1])?.status !== 'approved') return sendText(res, 404, 'Not found');
      const updated = await store.incrementMetric(downloadMatch[1], 'downloadCount');
      const meme = store.get(downloadMatch[1]);
      if (!meme) return sendText(res, 404, 'Not found');
      broadcastMetric(updated);
      res.setHeader('Content-Type', meme.mime);
      res.setHeader('Content-Disposition', `attachment; filename="${downloadName(meme)}"`);
      res.setHeader('X-Content-Type-Options', 'nosniff');
      return sendFile(res, store.absolutePath(meme.storageKey), { absolute: true });
    }

    if (isPublicRead && url.pathname === '/healthz') {
      noIndex(res);
      return sendJson(res, 200, { ok: true });
    }

    if (isBrowserPageRequest(req, url)) {
      return sendNotFoundPage(res, { baseUrl, pathname: url.pathname });
    }
    return sendText(res, 404, 'Not found');
  } catch (error) {
    const status = error.statusCode || 500;
    const message = status === 500 ? 'Internal server error.' : error.message;
    if (status === 429 && error.retryAfterSeconds) {
      res.setHeader('Retry-After', String(error.retryAfterSeconds));
    }
    if (status === 429 && error.rateLimit) {
      setRateLimitHeaders(res, error.rateLimit);
    }
    if (error.payload && typeof error.payload === 'object') {
      return sendJson(res, status, error.payload);
    }
    return sendJson(res, status, { error: message });
  }
});

server.listen(PORT, HOST, () => {
  console.log(`The Meme Protocol listening on http://${HOST}:${PORT}`);
});

function positiveInt(value, fallback) {
  const parsed = Number.parseInt(value || '', 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function isDiscoveryRoute(req, url) {
  if (!isReadRequest(req)) return false;
  return DISCOVERY_ROUTES.has(url.pathname);
}

function isBrowserPageRequest(req, url) {
  if (!isReadRequest(req)) return false;
  return ![
    '/api',
    '/assets',
    '/media',
    '/download',
    '/admin',
    '/admin-media'
  ].some((prefix) => url.pathname === prefix || url.pathname.startsWith(`${prefix}/`));
}

function isReadRequest(req) {
  return req.method === 'GET' || req.method === 'HEAD';
}

function sendNotFoundPage(res, { baseUrl, pathname }) {
  noIndex(res);
  return sendHtml(res, 404, renderNotFound({
    template: notFoundTemplate,
    baseUrl,
    pathname
  }));
}

async function loadDotEnv() {
  let content = '';
  try {
    content = await fs.readFile('.env', 'utf8');
  } catch (error) {
    if (error.code !== 'ENOENT') throw error;
    return;
  }

  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
    if (!match || process.env[match[1]] !== undefined) continue;
    process.env[match[1]] = unquoteEnvValue(match[2].trim());
  }
}

function unquoteEnvValue(value) {
  if (
    (value.startsWith('"') && value.endsWith('"')) ||
    (value.startsWith("'") && value.endsWith("'"))
  ) {
    return value.slice(1, -1);
  }
  return value;
}

function openApiSpecFor(baseUrl) {
  return {
    ...openApiSpec,
    servers: [{ url: baseUrl }]
  };
}

function renderMemeApiSkill(baseUrl) {
  return memeApiSkillTemplate.replaceAll('__API_BASE_URL__', baseUrl);
}

function downloadName(meme) {
  return `meme-protocol-${meme.id.slice(0, 12)}.${meme.ext}`;
}

function broadcastMetric(meme) {
  const payload = JSON.stringify({
    id: meme.id,
    viewCount: meme.viewCount,
    downloadCount: meme.downloadCount
  });
  for (const res of events) {
    res.write(`event: metric\ndata: ${payload}\n\n`);
  }
}

async function addMeme(req) {
  const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
  if (!Number.isFinite(contentLength) || contentLength <= 0) {
    const error = new Error('Missing upload size.');
    error.statusCode = 411;
    throw error;
  }
  if (contentLength > REQUEST_MAX_BYTES) {
    const error = new Error('Upload request is too large.');
    error.statusCode = 413;
    throw error;
  }
  const quota = await uploadLimiter.checkAndConsume(clientIp(req));

  const upload = await parseMultipartUpload(req, {
    maxRequestBytes: REQUEST_MAX_BYTES,
    maxFileBytes: UPLOAD_MAX_BYTES,
    fieldName: 'meme'
  });
  const image = validateImage(upload.buffer, {
    maxBytes: UPLOAD_MAX_BYTES,
    maxWidth: 6000,
    maxHeight: 6000,
    maxPixels: 20_000_000
  });
  const normalized = await normalizeToWebp(upload.buffer);
  const moderation = await moderateImage({ buffer: normalized.buffer, mime: normalized.image.mime });
  if (moderation.status === 'rejected') {
    const error = new Error(`Upload rejected: ${moderation.reason}`);
    error.statusCode = 422;
    error.payload = {
      error: error.message,
      moderationScore: moderation.score,
      moderationReason: moderation.reason
    };
    throw error;
  }
  const meme = await store.save({
    buffer: normalized.buffer,
    image: normalized.image,
    originalName: upload.filename,
    originalMime: image.mime,
    status: moderation.status,
    moderationScore: moderation.score,
    moderationReason: moderation.reason
  });
  return { meme, quota };
}

function checkReadLimit(req, res, limiter) {
  const quota = limiter.check(clientIp(req));
  setRateLimitHeaders(res, quota);
  return quota;
}

function setRateLimitHeaders(res, quota) {
  res.setHeader('RateLimit-Limit', String(quota.limit));
  res.setHeader('RateLimit-Remaining', String(quota.remaining));
  res.setHeader('RateLimit-Reset', quota.resetAt);
}

function publicMeme(record) {
  return {
    id: record.id,
    createdAt: record.createdAt,
    byteSize: record.byteSize,
    width: record.width,
    height: record.height,
    mime: record.mime,
    originalMime: record.originalMime,
    status: record.status,
    moderationScore: record.moderationScore,
    moderationReason: record.moderationReason,
    viewCount: record.viewCount,
    downloadCount: record.downloadCount,
    originalName: record.originalName,
    url: `/media/${record.id}`,
    downloadUrl: `/download/${record.id}`,
    demo: Boolean(record.demo)
  };
}

function isAdminRequest(req) {
  return isAdminToken(req.headers['x-admin-token'] || '');
}

function isAdminToken(value) {
  const received = Buffer.from(String(value));
  const expected = Buffer.from(ADMIN_TOKEN);
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

async function readJsonBody(req, maxBytes) {
  const chunks = [];
  let total = 0;
  for await (const chunk of req) {
    total += chunk.length;
    if (total > maxBytes) {
      const error = new Error('Request body is too large.');
      error.statusCode = 413;
      throw error;
    }
    chunks.push(chunk);
  }
  if (total === 0) return {};
  return JSON.parse(Buffer.concat(chunks, total).toString('utf8'));
}

function safeIds(ids) {
  if (!Array.isArray(ids)) return [];
  return ids.filter((id) => typeof id === 'string' && /^[a-f0-9]{64}$/.test(id));
}

function clientIp(req) {
  if (process.env.TRUST_PROXY === 'true') {
    const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
    if (forwarded) return forwarded;
  }
  return req.socket.remoteAddress || 'unknown';
}
