import { Buffer } from 'node:buffer';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { Elysia } from 'elysia';
import { WebStandardAdapter } from 'elysia/adapter/web-standard';

type ContentType = 'news' | 'article' | 'video' | 'audio' | 'graphic' | 'event';
type ContentStatus = 'Черновик' | 'На модерации' | 'На проверке' | 'Опубликовано' | 'Возвращен' | 'Архив';
type Visibility = 'Публично' | 'После входа' | 'По роли';
type RoleCode = 'администратор' | 'редактор' | 'менеджер' | 'пользователь';

type ContentItem = {
  id: string;
  title: string;
  lead: string;
  body: string;
  type: ContentType;
  category: string;
  tags: string[];
  author: string;
  publishedAt: string;
  duration?: string;
  visibility: Visibility;
  status: ContentStatus;
  views: number;
  imageTone: string;
  mediaUrl?: string;
  mediaKind?: 'image' | 'video' | 'audio' | 'document' | 'other';
  mimeType?: string;
  fileName?: string;
  fileSize?: number;
  moderatorComment?: string;
  reviewComment?: string;
  ratingAverage?: number;
  ratingCount?: number;
  myRating?: number;
};

type MediaKind = NonNullable<ContentItem['mediaKind']>;

type StoredUpload = {
  id: string;
  name: string;
  mimeType: string;
  size: number;
  data: Buffer;
};

type NormalizedContentPatch = Partial<ContentItem> & {
  title?: string;
  category?: string;
  lead?: string;
  body?: string;
  type?: ContentType;
  status?: ContentStatus;
  tags?: string[];
  moderatorComment?: string;
  reviewComment?: string;
};

type UserProfile = {
  id: string;
  name: string;
  login: string;
  roles: RoleCode[];
  subscriptions: string[];
};

type SetContext = {
  status?: number | string;
  headers: Record<string, string | number | undefined>;
};

const port = Number(process.env.GATEWAY_PORT ?? process.env.PORT ?? 3000);
const corsOrigin = process.env.CORS_ORIGIN ?? '*';
const rateLimitWindowMs = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60_000);
const rateLimitMax = Number(process.env.RATE_LIMIT_MAX ?? 180);

const serviceUrls: Record<string, string | undefined> = {
  auth: process.env.AUTH_SERVICE_URL,
  user: process.env.USER_SERVICE_URL,
  content: process.env.CONTENT_SERVICE_URL,
  taxonomy: process.env.TAXONOMY_SERVICE_URL,
  speaker: process.env.SPEAKER_SERVICE_URL,
  subscription: process.env.SUBSCRIPTION_SERVICE_URL,
  notification: process.env.NOTIFICATION_SERVICE_URL,
  comment: process.env.COMMENT_SERVICE_URL,
  search: process.env.SEARCH_SERVICE_URL,
  analytics: process.env.ANALYTICS_SERVICE_URL,
  audit: process.env.AUDIT_SERVICE_URL,
  media: process.env.MEDIA_SERVICE_URL
};

const categories = ['Новости', 'Статьи', 'Видео', 'Аудио', 'Графика', 'Мероприятия'];
const tags = ['медиапроизводство', 'интервью', 'анонс', 'образование', 'редакция', 'архив'];
const contentTypes: ContentType[] = ['news', 'article', 'video', 'audio', 'graphic', 'event'];
const loginAliases: Record<string, string> = {
  'admin@dstu.ru': 'demo_admin',
  'editor@dstu.ru': 'demo_editor',
  'moderator@dstu.ru': 'demo_moderator',
  'manager@dstu.ru': 'demo_moderator',
  'user@dstu.ru': 'demo_user'
};
const statusAliases: Record<string, ContentStatus> = {
  draft: 'Черновик',
  moderation: 'На модерации',
  pending: 'На модерации',
  review: 'На проверке',
  published: 'Опубликовано',
  returned: 'Возвращен',
  archived: 'Архив',
  черновик: 'Черновик',
  'на модерации': 'На модерации',
  'на проверке': 'На проверке',
  опубликовано: 'Опубликовано',
  возвращен: 'Возвращен',
  архив: 'Архив'
};

const demoUser: UserProfile = {
  id: 'demo-user-1',
  name: 'Демо-администратор',
  login: 'demo_admin',
  roles: ['администратор', 'редактор'],
  subscriptions: ['Новости', 'Демо-спикер 01', 'медиапроизводство']
};

const users: UserProfile[] = [
  demoUser,
  { id: 'demo-user-2', name: 'Демо-редактор', login: 'demo_editor', roles: ['редактор'], subscriptions: ['Видео'] },
  { id: 'demo-user-4', name: 'Демо-модератор', login: 'demo_moderator', roles: ['менеджер'], subscriptions: ['Мероприятия'] },
  { id: 'demo-user-3', name: 'Демо-пользователь', login: 'demo_user', roles: ['пользователь'], subscriptions: ['Аудио'] }
];

let content: ContentItem[] = [
  {
    id: 'demo-news-1',
    title: 'Демо-новость о запуске медиаплатформы',
    lead: 'Публичная карточка показывает, как новости и статьи будут выглядеть в едином каталоге.',
    body: 'Демонстрационный материал без реальных персональных данных, подразделений и брендовых материалов.',
    type: 'news',
    category: 'Новости',
    tags: ['медиапроизводство', 'анонс'],
    author: 'Демо-редакция',
    publishedAt: '2026-06-04',
    visibility: 'Публично',
    status: 'Опубликовано',
    views: 1240,
    imageTone: 'from-university-700 via-university-500 to-sky-300'
  },
  {
    id: 'demo-video-1',
    title: 'Демо-видео: открытая лекция',
    lead: 'Видеоматериал с метаданными, статусом проверки, категорией и тегами.',
    body: 'В реальной системе здесь будет предпросмотр видео, CDN-ссылка, история модерации и аналитика просмотров.',
    type: 'video',
    category: 'Видео',
    tags: ['образование', 'архив'],
    author: 'Демо-медиагруппа',
    publishedAt: '2026-06-09',
    duration: '18:40',
    visibility: 'После входа',
    status: 'На проверке',
    views: 382,
    imageTone: 'from-indigo-700 via-university-800 to-cyan-500'
  },
  {
    id: 'demo-audio-1',
    title: 'Демо-аудио: выпуск университетского радио',
    lead: 'Аудиоконтент хранится в медиатеке и связывается с публикациями, авторами и тегами.',
    body: 'Этот пример показывает карточку аудио без использования реального названия передачи или записи.',
    type: 'audio',
    category: 'Аудио',
    tags: ['интервью', 'редакция'],
    author: 'Демо-редактор',
    publishedAt: '2026-06-11',
    duration: '32:10',
    visibility: 'Публично',
    status: 'Опубликовано',
    views: 715,
    imageTone: 'from-blue-950 via-blue-700 to-emerald-300'
  },
  {
    id: 'demo-graphic-1',
    title: 'Демо-графика: афиша редакционного события',
    lead: 'Графические материалы можно фильтровать по типу, дате, категории и тегам.',
    body: 'Заглушка демонстрирует графический материал без копирования фотографий, логотипов или брендовых элементов.',
    type: 'graphic',
    category: 'Графика',
    tags: ['анонс', 'редакция'],
    author: 'Демо-дизайнер',
    publishedAt: '2026-06-13',
    visibility: 'По роли',
    status: 'На модерации',
    views: 96,
    imageTone: 'from-sky-400 via-blue-600 to-slate-900'
  },
  {
    id: 'demo-event-1',
    title: 'Демо-анонс медиавстречи со спикером',
    lead: 'Мероприятия показаны как тип медиаконтента до подтверждения отдельной сущности Event.',
    body: 'События представлены как анонсы контента, потому что отдельная сущность Event требует подтверждения заказчиком.',
    type: 'event',
    category: 'Мероприятия',
    tags: ['анонс', 'интервью'],
    author: 'Демо-менеджер',
    publishedAt: '2026-06-17',
    visibility: 'Публично',
    status: 'Черновик',
    views: 45,
    imageTone: 'from-university-900 via-violet-700 to-orange-300'
  }
];

const speakers = [
  { id: 'demo-speaker-1', name: 'Демо-спикер 01', role: 'Приглашенный эксперт', topics: ['медиапроизводство', 'образование'], materials: 8, subscribers: 132 },
  { id: 'demo-speaker-2', name: 'Демо-спикер 02', role: 'Участник редакционного события', topics: ['интервью', 'анонс'], materials: 5, subscribers: 74 },
  { id: 'demo-speaker-3', name: 'Демо-спикер 03', role: 'Автор образовательных материалов', topics: ['архив', 'редакция'], materials: 12, subscribers: 205 }
];

const notifications = [
  { id: 'demo-notification-1', title: 'Новый материал по подписке', description: 'В категории «Новости» появился демонстрационный материал.', read: false, createdAt: '2026-06-13 10:20' },
  { id: 'demo-notification-2', title: 'Материал ожидает проверки', description: 'Демо-видео находится на этапе проверки перед публикацией.', read: true, createdAt: '2026-06-12 16:45' }
];

const comments = [
  { id: 'demo-comment-1', contentId: 'demo-news-1', author: 'Демо-пользователь', text: 'Комментарий доступен авторизованным пользователям и может проходить модерацию.', createdAt: '2026-06-13 12:00' }
];

const audit = [
  { id: 'demo-audit-1', actor: 'Демо-администратор', action: 'изменил статус', target: 'Демо-видео: открытая лекция', createdAt: '2026-06-13 11:10' },
  { id: 'demo-audit-2', actor: 'Демо-редактор', action: 'создал черновик', target: 'Демо-анонс медиавстречи со спикером', createdAt: '2026-06-12 15:35' }
];

const tokens = new Map<string, UserProfile>();
const rateBuckets = new Map<string, { count: number; resetAt: number }>();
const uploadedFiles = new Map<string, StoredUpload>();

function readServiceToken(token: string): UserProfile | null {
  if (!token.startsWith('demo-token-')) {
    return null;
  }
  try {
    const encoded = token.slice('demo-token-'.length).replace(/-/g, '+').replace(/_/g, '/');
    const padded = encoded.padEnd(Math.ceil(encoded.length / 4) * 4, '=');
    const binary = atob(padded);
    const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
    const parsed = JSON.parse(new TextDecoder().decode(bytes)) as Pick<UserProfile, 'id' | 'name' | 'login' | 'roles'>;
    const stored = users.find((item) => item.id === parsed.id);
    return {
      id: parsed.id,
      name: parsed.name,
      login: parsed.login,
      roles: parsed.roles,
      subscriptions: stored?.subscriptions ?? []
    };
  } catch {
    return null;
  }
}

function fail(set: SetContext, status: number, code: string, message: string) {
  set.status = status;
  return {
    error: {
      code,
      message,
      requestId: set.headers['x-request-id'] ? String(set.headers['x-request-id']) : null
    }
  };
}

function readToken(request: Request) {
  const header = request.headers.get('authorization');
  if (!header?.startsWith('Bearer ')) {
    return null;
  }
  return header.slice('Bearer '.length);
}

function readUser(request: Request) {
  const token = readToken(request);
  if (!token) {
    return null;
  }
  if (token === 'demo-token-local-fallback') {
    return demoUser;
  }
  return tokens.get(token) ?? readServiceToken(token);
}

function requireUser(request: Request, set: SetContext) {
  const user = readUser(request);
  if (!user) {
    return { user: null, response: fail(set, 401, 'UNAUTHORIZED', 'Требуется аутентификация') };
  }
  return { user, response: null };
}

function requireRole(request: Request, set: SetContext, role: RoleCode) {
  const auth = requireUser(request, set);
  if (!auth.user) {
    return auth;
  }
  if (!auth.user.roles.includes(role)) {
    return { user: null, response: fail(set, 403, 'FORBIDDEN', 'Недостаточно прав доступа') };
  }
  return auth;
}

function applyRateLimit(request: Request, set: SetContext) {
  const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'local';
  const now = Date.now();
  const current = rateBuckets.get(ip);

  if (!current || current.resetAt <= now) {
    rateBuckets.set(ip, { count: 1, resetAt: now + rateLimitWindowMs });
    return undefined;
  }

  current.count += 1;
  if (current.count > rateLimitMax) {
    return fail(set, 429, 'RATE_LIMITED', 'Слишком много запросов');
  }

  return undefined;
}

function searchContent(query: Record<string, string | undefined>) {
  const term = (query.q ?? query.query ?? '').trim().toLowerCase();
  const category = query.category;
  const type = query.type as ContentType | undefined;
  const sorted = [...content]
    .filter((item) => !term || [item.title, item.lead, item.body, item.author, item.category, item.status, ...item.tags].join(' ').toLowerCase().includes(term))
    .filter((item) => !category || category === 'Все' || item.category === category)
    .filter((item) => !type || item.type === type)
    .sort((a, b) => (query.sort === 'newest' ? new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime() : b.views - a.views));

  return sorted;
}

function normalizeLogin(value = '') {
  const login = value.trim().toLowerCase();
  return loginAliases[login] ?? login;
}

function normalizeStatus(value: unknown): ContentStatus | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }
  return statusAliases[value.trim().toLowerCase()] ?? (value as ContentStatus);
}

function normalizeContentType(value: unknown): ContentType | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim().toLowerCase();
  if (normalized === 'research') return 'article';
  if (normalized === 'announcement') return 'news';
  if (contentTypes.includes(normalized as ContentType)) return normalized as ContentType;
  return undefined;
}

function normalizeTags(value: unknown) {
  if (Array.isArray(value)) {
    return value.map((item) => String(item).trim()).filter(Boolean);
  }
  if (typeof value === 'string') {
    return parseTags(value);
  }
  return [];
}

function normalizeContentPatch(payload: Record<string, unknown>): NormalizedContentPatch {
  const nextType = normalizeContentType(payload.type);
  const nextStatus = normalizeStatus(payload.status);
  const nextTags = payload.tags === undefined ? undefined : normalizeTags(payload.tags);

  return {
    title: typeof payload.title === 'string' ? payload.title.trim() : undefined,
    category: typeof payload.category === 'string' ? payload.category.trim() : undefined,
    lead: typeof payload.lead === 'string' ? payload.lead : typeof payload.excerpt === 'string' ? payload.excerpt : undefined,
    body: typeof payload.body === 'string' ? payload.body : typeof payload.content === 'string' ? payload.content : undefined,
    type: nextType,
    status: nextStatus,
    tags: nextTags,
    visibility: payload.visibility as Visibility | undefined,
    imageTone: typeof payload.imageTone === 'string' ? payload.imageTone : undefined,
    mediaUrl: typeof payload.mediaUrl === 'string' ? payload.mediaUrl : undefined,
    mediaKind: payload.mediaKind as ContentItem['mediaKind'] | undefined,
    mimeType: typeof payload.mimeType === 'string' ? payload.mimeType : undefined,
    fileName: typeof payload.fileName === 'string' ? payload.fileName : undefined,
    fileSize: typeof payload.fileSize === 'number' ? payload.fileSize : undefined,
    ratingAverage: typeof payload.ratingAverage === 'number' ? payload.ratingAverage : undefined,
    ratingCount: typeof payload.ratingCount === 'number' ? payload.ratingCount : undefined,
    myRating: typeof payload.myRating === 'number' ? payload.myRating : undefined,
    moderatorComment:
      typeof payload.moderatorComment === 'string'
        ? payload.moderatorComment.trim()
        : typeof payload.reviewComment === 'string'
          ? payload.reviewComment.trim()
          : undefined
  };
}

function listContent(query: Record<string, string | undefined>, user: UserProfile | null) {
  const requestedType = normalizeContentType(query.type);
  const requestedStatus = normalizeStatus(query.status);
  const requestedAuthor = query.authorId;
  const mine = query.mine === 'true';
  const limit = Number(query.limit ?? 0);
  const exclude = query.exclude;
  const term = (query.q ?? '').trim().toLowerCase();

  const items = content
    .filter((item) => !exclude || item.id !== exclude)
    .filter((item) => !requestedType || item.type === requestedType)
    .filter((item) => !query.category || item.category === query.category)
    .filter((item) => !requestedStatus || item.status === requestedStatus)
    .filter((item) => !requestedAuthor || item.author === users.find((entry) => entry.id === requestedAuthor)?.name)
    .filter((item) => !mine || (user ? item.author === user.name : false))
    .filter((item) => !term || [item.title, item.lead, item.body, item.author, item.category, item.status, ...item.tags].join(' ').toLowerCase().includes(term))
    .sort((a, b) => {
      if (query.sort === 'popular') return b.views - a.views;
      return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
    });

  return limit > 0 ? items.slice(0, limit) : items;
}

function isBlobLike(value: unknown): value is Blob & { name?: string; type?: string; size: number } {
  return typeof value === 'object' && value !== null && 'arrayBuffer' in value && typeof (value as { arrayBuffer?: unknown }).arrayBuffer === 'function';
}

function readStringField(source: FormData | Record<string, unknown>, key: string) {
  const rawValue = source instanceof FormData ? source.get(key) : source[key];
  const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
  if (typeof value === 'string') {
    return value.trim();
  }
  if (typeof value === 'number' || typeof value === 'boolean') {
    return String(value);
  }
  return '';
}

function readFileField(source: FormData | Record<string, unknown>, key: string) {
  const rawValue = source instanceof FormData ? source.get(key) : source[key];
  const value = Array.isArray(rawValue) ? rawValue.find(isBlobLike) : rawValue;
  return isBlobLike(value) ? value : null;
}

async function readUploadSource(request: Request, body: unknown): Promise<FormData | Record<string, unknown>> {
  if (body instanceof FormData) {
    return body;
  }
  if (body && typeof body === 'object') {
    return body as Record<string, unknown>;
  }
  return request.formData();
}

function inferMediaKind(mimeType = '', fileName = ''): MediaKind {
  const lowerName = fileName.toLowerCase();
  if (mimeType.startsWith('image/')) return 'image';
  if (mimeType.startsWith('video/')) return 'video';
  if (mimeType.startsWith('audio/')) return 'audio';
  if (mimeType === 'application/pdf' || /\.(pdf|doc|docx|ppt|pptx|xls|xlsx|txt|rtf)$/i.test(lowerName)) return 'document';
  return 'other';
}

function defaultContentTypeForMedia(kind: MediaKind): ContentType {
  if (kind === 'video') return 'video';
  if (kind === 'audio') return 'audio';
  if (kind === 'image') return 'graphic';
  return 'article';
}

function defaultCategoryForMedia(kind: MediaKind) {
  if (kind === 'video') return 'Видео';
  if (kind === 'audio') return 'Аудио';
  if (kind === 'image') return 'Графика';
  return 'Статьи';
}

function parseTags(value: string) {
  const tags = value
    .split(/[,;\n]/)
    .map((tag) => tag.trim())
    .filter(Boolean);
  return tags.length ? [...new Set(tags)] : ['демо', 'файл'];
}

async function readServiceHealth(name: string, url: string | undefined) {
  if (!url) {
    return { name, status: 'not_configured' };
  }
  try {
    const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1500) });
    return { name, status: response.ok ? 'ok' : 'error', url };
  } catch {
    return { name, status: 'unreachable', url };
  }
}

function serviceForAPIPath(pathname: string) {
  if (!pathname.startsWith('/api/') || pathname === '/api/health' || pathname === '/api/services/health') {
    return null;
  }

  const path = pathname.slice('/api'.length);
  if (path.startsWith('/auth/')) return 'auth';
  if (path === '/users' || path.startsWith('/users/') || path === '/admin/users' || path === '/roles' || path === '/admin/roles') return 'user';
  if (path === '/content' || path.startsWith('/content/') || path === '/events') return 'content';
  if (path === '/media' || path.startsWith('/media/')) return 'media';
  if (path === '/categories' || path === '/tags') return 'taxonomy';
  if (path === '/speakers' || path.startsWith('/speakers/')) return 'speaker';
  if (path === '/subscriptions' || path.startsWith('/subscriptions/')) return 'subscription';
  if (path === '/notifications' || path.startsWith('/notifications/')) return 'notification';
  if (path.startsWith('/comments/')) return 'comment';
  if (path === '/search' || path.startsWith('/search/')) return 'search';
  if (path === '/analytics/summary' || path === '/admin/dashboard') return 'analytics';
  if (path === '/admin/audit' || path.startsWith('/audit')) return 'audit';
  return null;
}

function encodeProxyBody(body: unknown, headers: Headers) {
  if (body === undefined || body === null) {
    return undefined;
  }
  if (typeof body === 'string' || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData || body instanceof URLSearchParams) {
    return body;
  }
  if (headers.get('content-type')?.includes('multipart/form-data') && typeof body === 'object') {
    const form = new FormData();
    for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
      if (Array.isArray(value)) {
        for (const entry of value) {
          if (isBlobLike(entry)) form.append(key, entry, entry.name ?? 'uploaded-file');
          else if (entry !== undefined && entry !== null) form.append(key, String(entry));
        }
      } else if (isBlobLike(value)) {
        form.append(key, value, value.name ?? 'uploaded-file');
      } else if (value !== undefined && value !== null) {
        form.append(key, String(value));
      }
    }
    headers.delete('content-type');
    return form;
  }
  if (!headers.has('content-type')) {
    headers.set('content-type', 'application/json');
  }
  return JSON.stringify(body);
}

async function proxyAPIRequest(request: Request, set: SetContext, body: unknown) {
  const source = new URL(request.url);
  const serviceName = serviceForAPIPath(source.pathname);
  if (!serviceName) {
    return undefined;
  }

  const serviceURL = serviceUrls[serviceName];
  if (!serviceURL) {
    return undefined;
  }

  try {
    const target = new URL(serviceURL);
    target.pathname = source.pathname.replace(/^\/api/, '') || '/';
    target.search = source.search;

    const headers = new Headers(request.headers);
    const requestId = set.headers['x-request-id'];
    if (requestId) {
      headers.set('x-request-id', String(requestId));
    }
    headers.set('x-forwarded-by', 'fable-gateway');

    const init: RequestInit = {
      method: request.method,
      headers,
      signal: AbortSignal.timeout(1800)
    };
    if (request.method !== 'GET' && request.method !== 'HEAD') {
      init.body = encodeProxyBody(body, headers);
    }

    const response = await fetch(target, init);
    const responseHeaders = new Headers(response.headers);
    for (const [key, value] of Object.entries(set.headers)) {
      if (value !== undefined && !responseHeaders.has(key)) {
        responseHeaders.set(key, String(value));
      }
    }

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: responseHeaders
    });
  } catch {
    return undefined;
  }
}

function createNodeRequest(req: IncomingMessage) {
  const host = req.headers.host ?? `localhost:${port}`;
  const url = new URL(req.url ?? '/', `http://${host}`);
  const headers = new Headers();

  for (const [key, value] of Object.entries(req.headers)) {
    if (Array.isArray(value)) {
      for (const entry of value) headers.append(key, entry);
    } else if (value !== undefined) {
      headers.set(key, value);
    }
  }

  const init: RequestInit & { duplex?: 'half' } = {
    method: req.method,
    headers
  };

  if (req.method !== 'GET' && req.method !== 'HEAD') {
    init.body = req as unknown as BodyInit;
    init.duplex = 'half';
  }

  return new Request(url, init);
}

async function writeNodeResponse(res: ServerResponse, response: Response) {
  res.statusCode = response.status;
  res.statusMessage = response.statusText;
  response.headers.forEach((value, key) => {
    res.setHeader(key, value);
  });

  if (!response.body) {
    res.end();
    return;
  }

  const body = Buffer.from(await response.arrayBuffer());
  res.end(body);
}

const app = new Elysia({ adapter: WebStandardAdapter })
  .onRequest(({ request, set }) => {
    const requestId = request.headers.get('x-request-id') ?? crypto.randomUUID();
    set.headers['x-request-id'] = requestId;
    set.headers['access-control-allow-origin'] = corsOrigin;
    set.headers['access-control-allow-methods'] = 'GET,POST,PATCH,DELETE,OPTIONS';
    set.headers['access-control-allow-headers'] = 'authorization,content-type,x-request-id';
    set.headers['access-control-expose-headers'] = 'x-request-id';
  })
  .onBeforeHandle(async ({ request, set, body }) => applyRateLimit(request, set) ?? (await proxyAPIRequest(request, set, body)))
  .onError(({ error, set }) => fail(set, 500, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Неизвестная ошибка'))
  .options('*', () => new Response(null, { status: 204 }))
  .get('/health', ({ set }) => ({ status: 'ok', service: 'gateway', requestId: set.headers['x-request-id'] }))
  .get('/api/health', ({ set }) => ({ status: 'ok', service: 'gateway', requestId: set.headers['x-request-id'] }))
  .get('/api/services/health', async () => ({ items: await Promise.all(Object.entries(serviceUrls).map(([name, url]) => readServiceHealth(name, url))) }))
  .post('/api/auth/register', ({ body, set }) => {
    const payload = body as Partial<{ login: string; email: string; password: string; name: string }>;
    const login = normalizeLogin(payload.login ?? payload.email ?? '');
    if (!login || !payload.password || payload.password.length < 8) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Укажите логин и пароль не короче 8 символов');
    }
    const user: UserProfile = {
      id: `demo-user-${Date.now()}`,
      name: payload.name?.trim() || 'Демо-пользователь',
      login,
      roles: ['пользователь'],
      subscriptions: []
    };
    users.push(user);
    const token = `demo-token-${crypto.randomUUID()}`;
    tokens.set(token, user);
    set.status = 201;
    return { token, user };
  })
  .post('/api/auth/login', ({ body, set }) => {
    const payload = body as Partial<{ login: string; email: string; password: string }>;
    const login = normalizeLogin(payload.login ?? payload.email ?? '');
    if (!login || !payload.password) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Укажите логин и пароль');
    }
    const user = users.find((item) => item.login === login) ?? demoUser;
    const token = `demo-token-${crypto.randomUUID()}`;
    tokens.set(token, user);
    return { token, user };
  })
  .get('/api/auth/me', ({ request, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    return { user: auth.user };
  })
  .post('/api/auth/logout', ({ request }) => {
    const token = readToken(request);
    if (token) {
      tokens.delete(token);
    }
    return { ok: true };
  })
  .post('/api/auth/change-password', ({ request, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    const payload = body as Partial<{ nextPassword: string }>;
    if (!payload.nextPassword || payload.nextPassword.length < 8) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Новый пароль должен быть не короче 8 символов');
    }
    return { ok: true };
  })
  .get('/api/content', ({ query, request }) => ({ items: listContent(query as Record<string, string | undefined>, readUser(request)) }))
  .get('/api/content/:id', ({ params, set }) => {
    const item = content.find((entry) => entry.id === params.id);
    if (!item) {
      return fail(set, 404, 'NOT_FOUND', 'Материал не найден');
    }
    item.views += 1;
    return { item };
  })
  .post('/api/content', ({ request, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    if (!auth.user.roles.some((role) => role === 'администратор' || role === 'редактор' || role === 'менеджер')) {
      return fail(set, 403, 'FORBIDDEN', 'Создание материалов требует роли редактора, менеджера или администратора');
    }
    const payload = normalizeContentPatch(body as Record<string, unknown>);
    if (!payload.title || !payload.category || !payload.type) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Укажите название, категорию и тип материала');
    }
    const item: ContentItem = {
      id: `demo-content-${Date.now()}`,
      title: payload.title,
      lead: payload.lead ?? 'Демонстрационный черновик',
      body: payload.body ?? 'Демо-описание материала.',
      type: payload.type,
      category: payload.category,
      tags: payload.tags ?? [],
      author: auth.user.name,
      publishedAt: new Date().toISOString().slice(0, 10),
      visibility: payload.visibility ?? 'После входа',
      status: payload.status ?? 'Черновик',
      views: 0,
      imageTone: payload.imageTone ?? 'from-university-800 via-slate-700 to-sky-300',
      mediaUrl: payload.mediaUrl,
      mediaKind: payload.mediaKind,
      mimeType: payload.mimeType,
      fileName: payload.fileName,
      fileSize: payload.fileSize,
      moderatorComment: payload.moderatorComment,
      reviewComment: payload.reviewComment,
      ratingAverage: 0,
      ratingCount: 0,
      myRating: 0
    };
    content = [item, ...content];
    set.status = 201;
    return { item };
  })
  .patch('/api/content/:id', ({ request, params, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    const index = content.findIndex((item) => item.id === params.id);
    if (index === -1) {
      return fail(set, 404, 'NOT_FOUND', 'Материал не найден');
    }
    const payload = normalizeContentPatch(body as Record<string, unknown>);
    const nextRating = typeof payload.ratingAverage === 'number' ? payload.ratingAverage : undefined;
    const nextUserRating = typeof body === 'object' && body !== null && typeof (body as Record<string, unknown>).rating === 'number'
      ? Number((body as Record<string, unknown>).rating)
      : undefined;

    content[index] = {
      ...content[index],
      ...payload,
      lead: payload.lead ?? content[index].lead,
      body: payload.body ?? content[index].body,
      tags: payload.tags ?? content[index].tags,
      status: payload.status ?? content[index].status,
      ratingAverage: nextUserRating ?? nextRating ?? content[index].ratingAverage,
      ratingCount: nextUserRating ? Math.max(Number(content[index].ratingCount ?? 0) + 1, 1) : content[index].ratingCount,
      myRating: nextUserRating ?? content[index].myRating,
      reviewComment: payload.moderatorComment ?? payload.reviewComment ?? content[index].reviewComment
    };
    return { item: content[index] };
  })
  .delete('/api/content/:id', ({ request, params, set }) => {
    const auth = requireRole(request, set, 'администратор');
    if (!auth.user) {
      return auth.response;
    }
    content = content.filter((item) => item.id !== params.id);
    return { ok: true };
  })
  .get('/api/media/files/:id', ({ params, set }) => {
    const file = uploadedFiles.get(params.id);
    if (!file) {
      return fail(set, 404, 'NOT_FOUND', 'Файл не найден');
    }
    const data = new Uint8Array(file.data.byteLength);
    data.set(file.data);
    return new Response(data, {
      headers: {
        'content-type': file.mimeType || 'application/octet-stream',
        'content-length': String(file.size),
        'content-disposition': `inline; filename="${file.name.replace(/"/g, '')}"`
      }
    });
  })
  .get('/api/media', () => ({ items: content.filter((item) => item.type === 'video' || item.type === 'audio' || item.type === 'graphic') }))
  .post('/api/media', async ({ request, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    if (!auth.user.roles.some((role) => role === 'администратор' || role === 'редактор' || role === 'менеджер')) {
      return fail(set, 403, 'FORBIDDEN', 'Создание материалов требует роли редактора, менеджера или администратора');
    }

    const source = await readUploadSource(request, body);
    const file = readFileField(source, 'file');
    if (!file) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Прикрепите файл');
    }

    const data = Buffer.from(await file.arrayBuffer());
    if (!data.length) {
      return fail(set, 400, 'INVALID_FILE', 'Файл пустой или поврежден');
    }

    const fileName = file.name || 'uploaded-file';
    const mimeType = file.type || 'application/octet-stream';
    const mediaKind = inferMediaKind(mimeType, fileName);
    const id = `demo-file-${Date.now()}`;
    uploadedFiles.set(id, { id, name: fileName, mimeType, size: data.byteLength, data });

    const requestedType = readStringField(source, 'type') as ContentType;
    const item: ContentItem = {
      id: `demo-media-${Date.now()}`,
      title: readStringField(source, 'title') || fileName,
      lead: readStringField(source, 'lead') || 'Демонстрационный медиаматериал с загруженным файлом.',
      body: readStringField(source, 'body') || 'Файл загружен в in-memory demo-хранилище gateway и доступен только до перезапуска процесса.',
      type: contentTypes.includes(requestedType) ? requestedType : defaultContentTypeForMedia(mediaKind),
      category: readStringField(source, 'category') || defaultCategoryForMedia(mediaKind),
      tags: parseTags(readStringField(source, 'tags')),
      author: auth.user.name,
      publishedAt: new Date().toISOString().slice(0, 10),
      visibility: 'После входа',
      status: 'Черновик',
      views: 0,
      imageTone: 'from-university-800 via-slate-700 to-sky-300',
      mediaUrl: `/api/media/files/${id}`,
      mediaKind,
      mimeType,
      fileName,
      fileSize: data.byteLength
    };

    content = [item, ...content];
    set.status = 201;
    return { item };
  })
  .get('/api/events', () => ({ items: content.filter((item) => item.type === 'event'), note: 'Event представлен как тип контента до подтверждения отдельной сущности.' }))
  .get('/api/categories', () => ({ items: categories }))
  .get('/api/tags', () => ({ items: tags }))
  .get('/api/speakers', () => ({ items: speakers }))
  .get('/api/search', ({ query }) => ({ items: searchContent(query as Record<string, string | undefined>) }))
  .get('/api/subscriptions', ({ request, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    return { items: auth.user.subscriptions };
  })
  .post('/api/subscriptions', ({ request, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    const payload = body as Partial<{ target: string }>;
    if (!payload.target) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Укажите объект подписки');
    }
    auth.user.subscriptions = [...new Set([...auth.user.subscriptions, payload.target])];
    return { items: auth.user.subscriptions };
  })
  .get('/api/notifications', () => ({ items: notifications }))
  .patch('/api/notifications/:id/read', ({ request, params, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    const item = notifications.find((entry) => entry.id === params.id);
    if (item) {
      item.read = true;
    }
    return { item };
  })
  .get('/api/comments/:contentId', ({ params }) => ({ items: comments.filter((item) => item.contentId === params.contentId) }))
  .post('/api/comments/:contentId', ({ request, params, body, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    const payload = body as Partial<{ text: string }>;
    if (!payload.text?.trim()) {
      return fail(set, 400, 'VALIDATION_ERROR', 'Комментарий не может быть пустым');
    }
    const item = { id: `demo-comment-${Date.now()}`, contentId: params.contentId, author: auth.user.name, text: payload.text.trim(), createdAt: new Date().toISOString() };
    comments.unshift(item);
    set.status = 201;
    return { item };
  })
  .get('/api/analytics/summary', ({ request, set }) => {
    const auth = requireUser(request, set);
    if (!auth.user) {
      return auth.response;
    }
    return {
      materials: content.filter((item) => item.author === auth.user?.name).length,
      comments: comments.length,
      totalViews: content.reduce((sum, item) => sum + item.views, 0),
      subscribers: speakers.reduce((sum, item) => sum + item.subscribers, 0),
      activeUsers: users.length,
      popular: [...content].sort((a, b) => b.views - a.views).slice(0, 3)
    };
  })
  .get('/api/admin/dashboard', ({ request, set }) => {
    const auth = requireRole(request, set, 'администратор');
    if (!auth.user) {
      return auth.response;
    }
    return {
      users: users.length,
      materials: content.length,
      views: content.reduce((sum, item) => sum + item.views, 0),
      pending: content.filter((item) => item.status !== 'Опубликовано').length,
      viewsByDay: [
        { label: 'Пн', value: 120 },
        { label: 'Вт', value: 180 },
        { label: 'Ср', value: 160 },
        { label: 'Чт', value: 220 },
        { label: 'Пт', value: 260 },
        { label: 'Сб', value: 140 },
        { label: 'Вс', value: 110 }
      ],
      roles: ['администратор', 'редактор', 'менеджер', 'пользователь']
    };
  })
  .get('/api/admin/users', ({ request, set }) => {
    const auth = requireRole(request, set, 'администратор');
    if (!auth.user) {
      return auth.response;
    }
    return { items: users };
  })
  .get('/api/admin/audit', ({ request, set }) => {
    const auth = requireRole(request, set, 'администратор');
    if (!auth.user) {
      return auth.response;
    }
    return { items: audit };
  })
  .get('/api/admin/roles', ({ request, set }) => {
    const auth = requireRole(request, set, 'администратор');
    if (!auth.user) {
      return auth.response;
    }
    return {
      items: [
        { code: 'администратор', permissions: ['*'] },
        { code: 'редактор', permissions: ['content:create', 'content:update', 'comments:moderate'] },
        { code: 'менеджер', permissions: ['content:create', 'content:publish', 'subscriptions:manage'] },
        { code: 'пользователь', permissions: ['content:read', 'comments:create', 'subscriptions:create'] }
      ]
    };
  })
  .all('*', ({ set }) => fail(set, 404, 'NOT_FOUND', 'Маршрут не найден'));

const server = createServer(async (req, res) => {
  try {
    const request = createNodeRequest(req);
    const response = await app.fetch(request);
    await writeNodeResponse(res, response);
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown gateway error';
    res.statusCode = 500;
    res.setHeader('content-type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({ error: { code: 'INTERNAL_ERROR', message } }));
  }
});

server.listen(port, () => {
  console.log(`Fable Elysia gateway is running on Node at http://localhost:${port}`);
});
