import { app, net } from 'electron';
import { DataSource } from 'typeorm';
import { buildQueryHandlers } from '../cqrs/queries';
import {
  QueryType,
  QueryTypeKey,
  Query
} from '../cqrs/types';
import {
  issueToken,
  consumeToken,
  revokeToken,
  IssuedToken
} from './auth-store';
import { buildOpenApiDocument } from './openapi';
import { HttpError, RequestContext } from './http-helpers';
import { getDocsHtml, getScalarApiReferenceBundlePath } from './docs-html';
import { resolveDocusaurusRoute } from './docusaurus-static';
import { LocalApiSettings } from '../desktop-settings';

export interface RouteResponse {
  status: number;
  body: unknown;
  contentType?: string;
  filePath?: string;
  rawBody?: string;
}

export interface RouteContext {
  request: RequestContext;
  settings: LocalApiSettings;
  baseUrl: string;
  dataSource: DataSource | null;
  bodyBuffer: () => Promise<unknown>;
}

type RouteHandler = (context: RouteContext) => Promise<RouteResponse>;

interface RouteMatch {
  handler: RouteHandler;
  params: Record<string, string>;
  requiresAuth: boolean;
  requiresDatabase: boolean;
}

interface RouteDefinition {
  method: string;
  pattern: RegExp;
  paramKeys: string[];
  handler: RouteHandler;
  requiresAuth: boolean;
  requiresDatabase: boolean;
}

function compilePattern(template: string): { pattern: RegExp; paramKeys: string[] } {
  const paramKeys: string[] = [];
  const escaped = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => {
    if (match === '*' || match === '+' || match === '?')
      return `\\${match}`;

    return `\\${match}`;
  });
  const source = template.replace(/\{([^}]+)\}/g, (_full, key: string) => {
    paramKeys.push(key);
    return '([^/]+)';
  });

  void escaped;

  return { pattern: new RegExp(`^${source}$`), paramKeys };
}

function defineRoute(method: string, template: string, handler: RouteHandler, requiresAuth: boolean, requiresDatabase = true): RouteDefinition {
  const compiled = compilePattern(template);

  return { method: method.toUpperCase(), pattern: compiled.pattern, paramKeys: compiled.paramKeys, handler, requiresAuth, requiresDatabase };
}

function runQuery<T>(dataSource: DataSource, query: Query): Promise<T> {
  const handlers = buildQueryHandlers(dataSource) as Record<QueryTypeKey, (q: Query) => Promise<unknown>>;
  const handler = handlers[query.type as QueryTypeKey];

  if (!handler) {
    throw new HttpError(500, `No handler registered for query: ${query.type}`, 'UNKNOWN_QUERY');
  }

  return handler(query) as Promise<T>;
}

function requireDataSource(dataSource: DataSource | null): DataSource {
  if (!dataSource) {
    throw new HttpError(503, 'Database not initialised', 'DB_UNAVAILABLE');
  }

  return dataSource;
}

function clampInt(value: unknown, min: number, max: number, fallback: number): number {
  const parsed = typeof value === 'string' ? Number(value) : NaN;

  if (!Number.isFinite(parsed))
    return fallback;

  return Math.max(min, Math.min(max, Math.floor(parsed)));
}

function getTrailingPathParam(pathname: string, pattern: RegExp, name: string): string {
  const value = pattern.exec(pathname)?.[1];

  if (!value) {
    throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
  }

  return decodeURIComponent(value);
}

function getRequiredQueryParam(ctx: RouteContext, name: string): string {
  const value = ctx.request.url.searchParams.get(name)?.trim() ?? '';

  if (!value) {
    throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
  }

  return value;
}

function getRequiredTimestamp(ctx: RouteContext, name: string): number {
  const raw = getRequiredQueryParam(ctx, name);
  const value = Number(raw);

  if (!Number.isFinite(value) || value < 0) {
    throw new HttpError(400, `${name} must be a non-negative timestamp`, 'INVALID_REQUEST');
  }

  return Math.floor(value);
}

const ROUTES: RouteDefinition[] = [
  defineRoute('GET', '/api/health', async (ctx): Promise<RouteResponse> => ({
    status: 200,
    body: { status: 'ok', version: app.getVersion(), timestamp: Date.now(), exposeOnLan: ctx.settings.exposeOnLan }
  }), false, false),

  defineRoute('GET', '/api/openapi.json', async (ctx): Promise<RouteResponse> => ({
    status: 200,
    body: buildOpenApiDocument({ baseUrl: ctx.baseUrl, appVersion: app.getVersion() })
  }), false, false),

  defineRoute('GET', '/docs', async (ctx): Promise<RouteResponse> => {
    if (!ctx.settings.scalarEnabled) {
      return {
        status: 404,
        body: null,
        contentType: 'text/plain; charset=utf-8',
        rawBody: 'API documentation is disabled. Enable Scalar in desktop settings to view it.'
      };
    }

    return {
      status: 200,
      body: null,
      contentType: 'text/html; charset=utf-8',
      rawBody: getDocsHtml(`${ctx.baseUrl}/api/openapi.json`)
    };
  }, false, false),

  defineRoute('GET', '/docusaurus(?:/.*)?', async (ctx): Promise<RouteResponse> => {
    if (!ctx.settings.docusaurusEnabled) {
      return {
        status: 404,
        body: null,
        contentType: 'text/plain; charset=utf-8',
        rawBody: 'Docusaurus documentation is disabled. Open documentation from the desktop app to activate it.'
      };
    }

    const docsRoute = await resolveDocusaurusRoute(ctx.request.pathname);

    return {
      status: 200,
      body: null,
      contentType: docsRoute.contentType,
      filePath: docsRoute.filePath
    };
  }, false, false),

  defineRoute('GET', '/scalar/api-reference.js', async (ctx): Promise<RouteResponse> => {
    if (!ctx.settings.scalarEnabled) {
      return {
        status: 404,
        body: null,
        contentType: 'text/plain; charset=utf-8',
        rawBody: 'API documentation is disabled. Enable Scalar in desktop settings to view it.'
      };
    }

    const bundlePath = await getScalarApiReferenceBundlePath();

    if (!bundlePath) {
      throw new HttpError(503, 'Scalar API reference bundle is not available in this build', 'SCALAR_BUNDLE_MISSING');
    }

    return {
      status: 200,
      body: null,
      contentType: 'application/javascript; charset=utf-8',
      filePath: bundlePath
    };
  }, false, false),

  defineRoute('POST', '/api/auth/login', async (ctx): Promise<RouteResponse> => {
    const body = await ctx.bodyBuffer() as { username?: unknown; password?: unknown; serverUrl?: unknown };
    const username = typeof body.username === 'string' ? body.username.trim() : '';
    const password = typeof body.password === 'string' ? body.password : '';
    const serverUrl = typeof body.serverUrl === 'string' ? body.serverUrl.trim().replace(/\/+$/u, '') : '';

    if (!username || !password || !serverUrl) {
      throw new HttpError(400, 'username, password, and serverUrl are required', 'INVALID_REQUEST');
    }

    if (!/^https?:\/\//iu.test(serverUrl)) {
      throw new HttpError(400, 'serverUrl must be an http or https URL', 'INVALID_REQUEST');
    }

    if (ctx.settings.allowedSignalingServers.length === 0) {
      throw new HttpError(403, 'No signaling servers are allowed for local API authentication. Add one in desktop settings.', 'NO_ALLOWED_SERVERS');
    }

    if (!ctx.settings.allowedSignalingServers.includes(serverUrl)) {
      throw new HttpError(403, 'Signaling server URL is not in the allowed list', 'SERVER_NOT_ALLOWED');
    }

    let response: Response;

    try {
      response = await net.fetch(`${serverUrl}/api/users/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
      });
    } catch (error) {
      throw new HttpError(502, `Failed to reach signaling server: ${(error as Error).message}`, 'UPSTREAM_UNREACHABLE');
    }

    if (response.status === 401 || response.status === 403) {
      throw new HttpError(401, 'Invalid credentials', 'INVALID_CREDENTIALS');
    }

    if (!response.ok) {
      throw new HttpError(502, `Signaling server rejected login (${response.status})`, 'UPSTREAM_ERROR');
    }

    const remote = await response.json() as { id?: string; username?: string; displayName?: string };

    if (!remote.id || !remote.username) {
      throw new HttpError(502, 'Signaling server returned an unexpected response', 'UPSTREAM_BAD_RESPONSE');
    }

    const issued = issueToken({
      userId: remote.id,
      username: remote.username,
      displayName: remote.displayName ?? remote.username,
      signalingServerUrl: serverUrl
    });

    return {
      status: 200,
      body: {
        token: issued.token,
        expiresAt: issued.expiresAt,
        user: {
          id: issued.userId,
          username: issued.username,
          displayName: issued.displayName
        }
      }
    };
  }, false),

  defineRoute('POST', '/api/auth/logout', async (ctx): Promise<RouteResponse> => {
    if (ctx.request.bearerToken) {
      revokeToken(ctx.request.bearerToken);
    }

    return { status: 204, body: null };
  }, true),

  defineRoute('GET', '/api/profile', async (ctx): Promise<RouteResponse> => {
    const user = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetCurrentUser,
      payload: {}
    });

    if (!user) {
      throw new HttpError(404, 'No current user is set on this device', 'NO_CURRENT_USER');
    }

    return { status: 200, body: user };
  }, true),

  defineRoute('GET', '/api/rooms', async (ctx): Promise<RouteResponse> => {
    const rooms = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetAllRooms,
      payload: {}
    });

    return { status: 200, body: rooms ?? [] };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}', async (ctx): Promise<RouteResponse> => {
    const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)$/u, 'roomId');
    const room = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetRoom,
      payload: { roomId }
    });

    if (!room) {
      throw new HttpError(404, 'Room not found on this device', 'ROOM_NOT_FOUND');
    }

    return { status: 200, body: room };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}/users', async (ctx): Promise<RouteResponse> => {
    const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/users$/u, 'roomId');
    const users = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetUsersByRoom,
      payload: { roomId }
    });

    return { status: 200, body: users ?? [] };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}/messages', async (ctx): Promise<RouteResponse> => {
    const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages$/u, 'roomId');
    const limit = clampInt(ctx.request.url.searchParams.get('limit'), 1, 500, 100);
    const offset = clampInt(ctx.request.url.searchParams.get('offset'), 0, Number.MAX_SAFE_INTEGER, 0);
    const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetMessages,
      payload: { roomId, limit, offset }
    });

    return { status: 200, body: messages ?? [] };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}/messages/since', async (ctx): Promise<RouteResponse> => {
    const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages\/since$/u, 'roomId');
    const sinceTimestamp = getRequiredTimestamp(ctx, 'sinceTimestamp');
    const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetMessagesSince,
      payload: { roomId, sinceTimestamp }
    });

    return { status: 200, body: messages ?? [] };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}/bans', async (ctx): Promise<RouteResponse> => {
    const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/bans$/u, 'roomId');
    const bans = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetBansForRoom,
      payload: { roomId }
    });

    return { status: 200, body: bans ?? [] };
  }, true),

  defineRoute('GET', '/api/rooms/{roomId}/bans/{userId}', async (ctx): Promise<RouteResponse> => {
    const match = /\/api\/rooms\/([^/]+)\/bans\/([^/]+)$/u.exec(ctx.request.pathname);

    if (!match) {
      throw new HttpError(400, 'roomId and userId are required', 'INVALID_REQUEST');
    }

    const isBanned = await runQuery<boolean>(requireDataSource(ctx.dataSource), {
      type: QueryType.IsUserBanned,
      payload: { roomId: decodeURIComponent(match[1]), userId: decodeURIComponent(match[2]) }
    });

    return { status: 200, body: { isBanned } };
  }, true),

  defineRoute('GET', '/api/messages/{messageId}', async (ctx): Promise<RouteResponse> => {
    const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)$/u, 'messageId');
    const message = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetMessageById,
      payload: { messageId }
    });

    if (!message) {
      throw new HttpError(404, 'Message not found on this device', 'MESSAGE_NOT_FOUND');
    }

    return { status: 200, body: message };
  }, true),

  defineRoute('GET', '/api/messages/{messageId}/reactions', async (ctx): Promise<RouteResponse> => {
    const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/reactions$/u, 'messageId');
    const reactions = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetReactionsForMessage,
      payload: { messageId }
    });

    return { status: 200, body: reactions ?? [] };
  }, true),

  defineRoute('GET', '/api/messages/{messageId}/attachments', async (ctx): Promise<RouteResponse> => {
    const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/attachments$/u, 'messageId');
    const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetAttachmentsForMessage,
      payload: { messageId }
    });

    return { status: 200, body: attachments ?? [] };
  }, true),

  defineRoute('GET', '/api/users/{userId}', async (ctx): Promise<RouteResponse> => {
    const userId = getTrailingPathParam(ctx.request.pathname, /\/api\/users\/([^/]+)$/u, 'userId');
    const user = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetUser,
      payload: { userId }
    });

    if (!user) {
      throw new HttpError(404, 'User not found on this device', 'USER_NOT_FOUND');
    }

    return { status: 200, body: user };
  }, true),

  defineRoute('GET', '/api/attachments', async (ctx): Promise<RouteResponse> => {
    const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetAllAttachments,
      payload: {}
    });

    return { status: 200, body: attachments ?? [] };
  }, true),

  defineRoute('GET', '/api/plugin-data', async (ctx): Promise<RouteResponse> => {
    const pluginId = getRequiredQueryParam(ctx, 'pluginId');
    const key = getRequiredQueryParam(ctx, 'key');
    const scope = getRequiredQueryParam(ctx, 'scope');

    if (scope !== 'local' && scope !== 'server') {
      throw new HttpError(400, 'scope must be local or server', 'INVALID_REQUEST');
    }

    const value = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetPluginData,
      payload: {
        key,
        pluginId,
        scope,
        serverId: ctx.request.url.searchParams.get('serverId') ?? undefined
      }
    });

    return { status: 200, body: { value } };
  }, true),

  defineRoute('GET', '/api/meta/{key}', async (ctx): Promise<RouteResponse> => {
    const key = getTrailingPathParam(ctx.request.pathname, /\/api\/meta\/([^/]+)$/u, 'key');
    const value = await runQuery<string | null>(requireDataSource(ctx.dataSource), {
      type: QueryType.GetMeta,
      payload: { key }
    });

    return { status: 200, body: { key, value } };
  }, true)
];

export interface RoutingResult {
  match: RouteMatch | null;
  methodNotAllowed: boolean;
}

export function matchRoute(method: string, pathname: string): RoutingResult {
  let methodNotAllowed = false;

  for (const route of ROUTES) {
    const result = route.pattern.exec(pathname);

    if (!result)
      continue;

    if (route.method !== method) {
      methodNotAllowed = true;
      continue;
    }

    const params: Record<string, string> = {};

    for (let index = 0; index < route.paramKeys.length; index++) {
      params[route.paramKeys[index]] = result[index + 1];
    }

    return {
      match: { handler: route.handler, params, requiresAuth: route.requiresAuth, requiresDatabase: route.requiresDatabase },
      methodNotAllowed: false
    };
  }

  return { match: null, methodNotAllowed };
}

export function authenticate(token: string | null): IssuedToken | null {
  if (!token)
    return null;

  return consumeToken(token);
}
