import fs from 'fs';
import path from 'path';
import { execFileSync } from 'child_process';
import { timingSafeEqual } from 'crypto';

import { WEB_DASHBOARD } from './config.js';
import {
  createTask,
  deleteTask,
  getAllOpenPairedTasks,
  getAllTasks,
  getLatestPairedTaskForChat,
  getPairedTaskById,
  getPairedTurnAttempts,
  getPairedTurnOutputs,
  getRecentPairedTurnOutputsForChat,
  getPairedTurnsForTask,
  getLatestPairedTurnForTask,
  getRecentDeliveredWorkItemsForChat,
  getRecentChatMessages,
  getTaskById,
  hasMessage,
  storeChatMetadata,
  storeMessage,
  updatePairedTaskIfUnchanged,
  updateTask,
} from './db.js';
import { logger } from './logger.js';
import { schedulePairedFollowUpIntent } from './message-runtime-follow-up.js';
import {
  readStatusSnapshots,
  type StatusSnapshot,
} from './status-dashboard.js';
import type {
  AgentType,
  NewMessage,
  PairedTask,
  RegisteredGroup,
  ScheduledTask,
} from './types.js';
import {
  createInboxDismissTracker,
  handleInboxActionRoute,
  type InboxFollowUpScheduler,
} from './web-dashboard-inbox-routes.js';
import { handleOverviewRoute } from './web-dashboard-overview-routes.js';
import {
  createRoomMessageIdCache,
  handleRoomMessageRoute,
} from './web-dashboard-room-message-routes.js';
import {
  handleRoomTimelineRoute,
  startRoomsTimelineCacheRefresh,
  type RoomsTimelineRouteDependencies,
} from './web-dashboard-room-routes.js';
import { handleSimpleGetRoute } from './web-dashboard-routes.js';
import {
  handleServiceRoute,
  type ServiceRestartRecord,
} from './web-dashboard-service-routes.js';
import { handleSettingsRoute } from './web-dashboard-settings-routes.js';
import { handleScheduledTaskRoute } from './web-dashboard-task-routes.js';

const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
const STACK_RESTART_UNIT_NAME = 'ejclaw-stack-restart.service';
const MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE =
  'Stack restart unit is not installed yet. Run setup service from an external shell before retrying from a managed EJClaw service.';
const UNIT_NOT_FOUND_PATTERN =
  /(Unit .* not found|Could not find the requested service|not-found)/i;

type JsonResponse = (
  value: unknown,
  init?: ResponseInit,
  request?: Request,
) => Response;

export type { ServiceRestartRecord } from './web-dashboard-service-routes.js';

export interface WebDashboardHandlerOptions {
  staticDir?: string;
  statusMaxAgeMs?: number;
  startBackgroundCacheRefresh?: boolean;
  readStatusSnapshots?: (maxAgeMs: number) => StatusSnapshot[];
  getTasks?: () => ScheduledTask[];
  getTaskById?: (id: string) => ScheduledTask | undefined;
  updateTask?: (
    id: string,
    updates: Partial<
      Pick<
        ScheduledTask,
        | 'prompt'
        | 'schedule_type'
        | 'schedule_value'
        | 'next_run'
        | 'status'
        | 'suspended_until'
      >
    >,
  ) => void;
  createTask?: (task: {
    id: string;
    group_folder: string;
    chat_jid: string;
    agent_type?: AgentType | null;
    prompt: string;
    schedule_type: ScheduledTask['schedule_type'];
    schedule_value: string;
    context_mode: ScheduledTask['context_mode'];
    next_run: string | null;
    status: ScheduledTask['status'];
    created_at: string;
  }) => void;
  deleteTask?: (id: string) => void;
  getPairedTasks?: () => PairedTask[];
  getLatestPairedTaskForChat?: (chatJid: string) => PairedTask | undefined;
  getPairedTurnsForTask?: typeof getPairedTurnsForTask;
  getLatestPairedTurnForTask?: typeof getLatestPairedTurnForTask;
  getPairedTurnAttempts?: typeof getPairedTurnAttempts;
  getPairedTurnOutputs?: typeof getPairedTurnOutputs;
  getRecentPairedTurnOutputsForChat?: typeof getRecentPairedTurnOutputsForChat;
  getRecentDeliveredWorkItemsForChat?: typeof getRecentDeliveredWorkItemsForChat;
  getRecentChatMessages?: typeof getRecentChatMessages;
  getPairedTaskById?: (id: string) => PairedTask | undefined;
  updatePairedTaskIfUnchanged?: (
    id: string,
    expectedUpdatedAt: string,
    updates: Partial<
      Pick<
        PairedTask,
        'status' | 'updated_at' | 'arbiter_requested_at' | 'completion_reason'
      >
    >,
  ) => boolean;
  schedulePairedFollowUp?: InboxFollowUpScheduler;
  getRoomBindings?: () => Record<string, RegisteredGroup>;
  storeChatMetadata?: (
    chatJid: string,
    timestamp: string,
    name?: string,
    channel?: string,
    isGroup?: boolean,
  ) => void;
  storeMessage?: (message: NewMessage) => void;
  hasMessage?: (chatJid: string, id: string) => boolean;
  enqueueMessageCheck?: (chatJid: string, groupFolder: string) => void;
  nudgeScheduler?: () => void;
  restartServiceStack?: () => string[];
  authToken?: string;
  now?: () => string;
}

export interface StartedWebDashboardServer {
  url: string;
  stop: () => void;
}

function jsonResponse(
  value: unknown,
  init?: ResponseInit,
  request?: Request,
): Response {
  const body = JSON.stringify(value);
  const acceptsGzip =
    request?.headers.get('accept-encoding')?.includes('gzip') ?? false;
  const baseHeaders: Record<string, string> = {
    'content-type': 'application/json; charset=utf-8',
    ...(init?.headers as Record<string, string> | undefined),
  };
  if (acceptsGzip && body.length > 1024) {
    const compressed = Bun.gzipSync(new TextEncoder().encode(body));
    return new Response(compressed, {
      ...init,
      headers: {
        ...baseHeaders,
        'content-encoding': 'gzip',
        vary: 'accept-encoding',
      },
    });
  }
  return new Response(body, {
    ...init,
    headers: baseHeaders,
  });
}

function getContentType(filePath: string): string {
  const ext = path.extname(filePath).toLowerCase();
  switch (ext) {
    case '.html':
      return 'text/html; charset=utf-8';
    case '.js':
    case '.mjs':
      return 'text/javascript; charset=utf-8';
    case '.css':
      return 'text/css; charset=utf-8';
    case '.json':
      return 'application/json; charset=utf-8';
    case '.svg':
      return 'image/svg+xml';
    case '.png':
      return 'image/png';
    case '.jpg':
    case '.jpeg':
      return 'image/jpeg';
    case '.webp':
      return 'image/webp';
    case '.ico':
      return 'image/x-icon';
    default:
      return 'application/octet-stream';
  }
}

function resolveStaticFile(staticDir: string, pathname: string): string | null {
  const normalizedPath = decodeURIComponent(pathname).replace(/^\/+/, '');
  const candidate = path.resolve(staticDir, normalizedPath || 'index.html');
  const root = path.resolve(staticDir);
  if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) {
    return null;
  }

  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
    return candidate;
  }

  const indexPath = path.join(root, 'index.html');
  if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
    return indexPath;
  }

  return null;
}

function serveStaticFile(staticDir: string, pathname: string): Response {
  const filePath = resolveStaticFile(staticDir, pathname);
  if (!filePath) {
    return new Response('Not found', { status: 404 });
  }

  return new Response(fs.readFileSync(filePath), {
    headers: {
      'content-type': getContentType(filePath),
    },
  });
}

function extractDashboardAuthToken(request: Request): string {
  const authorization = request.headers.get('authorization') ?? '';
  const bearer = authorization.match(/^Bearer\s+(.+)$/i);
  if (bearer) return bearer[1]!.trim();
  return request.headers.get('x-ejclaw-dashboard-token')?.trim() ?? '';
}

function safeTokenEquals(actual: string, expected: string): boolean {
  const actualBytes = Buffer.from(actual);
  const expectedBytes = Buffer.from(expected);
  if (actualBytes.length !== expectedBytes.length) return false;
  return timingSafeEqual(actualBytes, expectedBytes);
}

function isDashboardApiAuthorized(request: Request, token: string): boolean {
  if (!token) return true;
  const actual = extractDashboardAuthToken(request);
  return Boolean(actual) && safeTokenEquals(actual, token);
}

function rejectUnauthorizedApi(
  url: URL,
  request: Request,
  token: string,
  jsonResponse: JsonResponse,
): Response | null {
  if (!url.pathname.startsWith('/api/')) return null;
  if (isDashboardApiAuthorized(request, token)) return null;
  return jsonResponse(
    { error: 'Unauthorized' },
    {
      status: 401,
      headers: { 'www-authenticate': 'Bearer' },
    },
  );
}

function isRoot(): boolean {
  return typeof process.getuid === 'function' && process.getuid() === 0;
}

function restartEjclawStackServices(): string[] {
  if (process.platform !== 'linux') {
    throw new Error('Service restart only supports Linux systemd services');
  }

  const services = ['ejclaw'];
  const systemctlArgs = isRoot() ? [] : ['--user'];

  try {
    execFileSync(
      'systemctl',
      [...systemctlArgs, 'start', '--wait', STACK_RESTART_UNIT_NAME],
      { stdio: 'ignore' },
    );
  } catch (error) {
    const stderr =
      error && typeof error === 'object' && 'stderr' in error
        ? String((error as { stderr?: unknown }).stderr || '')
        : '';
    const stdout =
      error && typeof error === 'object' && 'stdout' in error
        ? String((error as { stdout?: unknown }).stdout || '')
        : '';
    const message =
      error instanceof Error ? error.message : `${stdout}\n${stderr}`.trim();
    const combined = `${message}\n${stdout}\n${stderr}`;
    if (!UNIT_NOT_FOUND_PATTERN.test(combined)) {
      throw error;
    }
    if (process.env.SERVICE_ID) {
      throw new Error(MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE, {
        cause: error,
      });
    }

    execFileSync('systemctl', [...systemctlArgs, 'restart', ...services], {
      stdio: 'ignore',
    });
    for (const service of services) {
      execFileSync(
        'systemctl',
        [...systemctlArgs, 'is-active', '--quiet', service],
        { stdio: 'ignore' },
      );
    }
  }

  return services;
}

export function createWebDashboardHandler(
  opts: WebDashboardHandlerOptions = {},
): (request: Request) => Response | Promise<Response> {
  const readSnapshots = opts.readStatusSnapshots ?? readStatusSnapshots;
  const loadTasks = opts.getTasks ?? getAllTasks;
  const loadTaskById = opts.getTaskById ?? getTaskById;
  const mutateTask = opts.updateTask ?? updateTask;
  const createScheduledTask = opts.createTask ?? createTask;
  const removeTask = opts.deleteTask ?? deleteTask;
  const loadPairedTasks = opts.getPairedTasks ?? getAllOpenPairedTasks;
  const loadPairedTaskById = opts.getPairedTaskById ?? getPairedTaskById;
  const mutatePairedTaskIfUnchanged =
    opts.updatePairedTaskIfUnchanged ?? updatePairedTaskIfUnchanged;
  const loadLatestPairedTaskForChat =
    opts.getLatestPairedTaskForChat ?? getLatestPairedTaskForChat;
  const loadPairedTurnsForTask =
    opts.getPairedTurnsForTask ?? getPairedTurnsForTask;
  const loadLatestPairedTurnForTask =
    opts.getLatestPairedTurnForTask ?? getLatestPairedTurnForTask;
  const loadPairedTurnOutputs =
    opts.getPairedTurnOutputs ?? getPairedTurnOutputs;
  const loadRecentPairedTurnOutputsForChat =
    opts.getRecentPairedTurnOutputsForChat ?? getRecentPairedTurnOutputsForChat;
  const loadRecentDeliveredWorkItemsForChat =
    opts.getRecentDeliveredWorkItemsForChat ??
    getRecentDeliveredWorkItemsForChat;
  const loadPairedTurnAttempts =
    opts.getPairedTurnAttempts ?? getPairedTurnAttempts;
  const loadRecentChatMessages =
    opts.getRecentChatMessages ?? getRecentChatMessages;
  const schedulePairedFollowUp =
    opts.schedulePairedFollowUp ?? schedulePairedFollowUpIntent;
  const loadRoomBindings = opts.getRoomBindings;
  const writeChatMetadata = opts.storeChatMetadata ?? storeChatMetadata;
  const writeMessage = opts.storeMessage ?? storeMessage;
  const messageExists = opts.hasMessage ?? hasMessage;
  const enqueueMessageCheck = opts.enqueueMessageCheck;
  const nudgeScheduler = opts.nudgeScheduler;
  const restartServiceStack =
    opts.restartServiceStack ?? restartEjclawStackServices;
  const statusMaxAgeMs = opts.statusMaxAgeMs ?? DEFAULT_STATUS_MAX_AGE_MS;
  const authToken = opts.authToken ?? WEB_DASHBOARD.token;
  const roomsTimelineDeps: RoomsTimelineRouteDependencies = {
    statusMaxAgeMs,
    readSnapshots,
    loadLatestPairedTaskForChat,
    loadPairedTurnsForTask,
    loadLatestPairedTurnForTask,
    loadPairedTurnAttempts,
    loadPairedTurnOutputs,
    loadRecentPairedTurnOutputsForChat,
    loadRecentDeliveredWorkItemsForChat,
    loadRecentChatMessages,
  };
  if (opts.startBackgroundCacheRefresh !== false) {
    startRoomsTimelineCacheRefresh(roomsTimelineDeps);
  }
  const rememberRoomMessageId = createRoomMessageIdCache();
  const inboxDismissTracker = createInboxDismissTracker();
  const recentServiceRestarts: ServiceRestartRecord[] = [];
  const activeServiceRestartTargets = new Set<string>();
  return async (request: Request): Promise<Response> => {
    const url = new URL(request.url);
    const authFailure = rejectUnauthorizedApi(
      url,
      request,
      authToken,
      jsonResponse,
    );
    if (authFailure) return authFailure;
    const scheduledTaskRoute = await handleScheduledTaskRoute({
      url,
      request,
      jsonResponse,
      createScheduledTask,
      loadRoomBindings,
      loadTaskById,
      mutateTask,
      nudgeScheduler,
      removeTask,
      now: opts.now,
    });
    if (scheduledTaskRoute) return scheduledTaskRoute;
    const inboxActionRoute = await handleInboxActionRoute({
      url,
      request,
      jsonResponse,
      dismissTracker: inboxDismissTracker,
      enqueueMessageCheck,
      loadPairedTaskById,
      messageExists,
      mutatePairedTaskIfUnchanged,
      schedulePairedFollowUp,
      writeChatMetadata,
      writeMessage,
      now: opts.now,
    });
    if (inboxActionRoute) return inboxActionRoute;
    const roomMessageRoute = await handleRoomMessageRoute({
      url,
      request,
      jsonResponse,
      enqueueMessageCheck,
      loadRoomBindings,
      messageExists,
      rememberRoomMessageId,
      writeChatMetadata,
      writeMessage,
      now: opts.now,
    });
    if (roomMessageRoute) return roomMessageRoute;
    const roomTimelineRoute = handleRoomTimelineRoute({
      url,
      request,
      jsonResponse,
      ...roomsTimelineDeps,
    });
    if (roomTimelineRoute) return roomTimelineRoute;
    const serviceRoute = await handleServiceRoute({
      url,
      request,
      jsonResponse,
      recentServiceRestarts,
      activeServiceRestartTargets,
      restartServiceStack,
      now: opts.now,
    });
    if (serviceRoute) return serviceRoute;
    const settingsRoute = await handleSettingsRoute({
      url,
      request,
      jsonResponse,
    });
    if (settingsRoute) return settingsRoute;
    if (request.method !== 'GET' && request.method !== 'HEAD') {
      return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
    }
    const simpleGetRoute = handleSimpleGetRoute({
      url,
      statusMaxAgeMs,
      readSnapshots,
      loadTasks,
      jsonResponse,
    });
    if (simpleGetRoute) return simpleGetRoute;
    const overviewRoute = handleOverviewRoute({
      url,
      jsonResponse,
      isInboxItemDismissed: inboxDismissTracker.isDismissed,
      loadPairedTasks,
      loadTasks,
      readSnapshots,
      recentServiceRestarts,
      statusMaxAgeMs,
      now: opts.now,
    });
    if (overviewRoute) return overviewRoute;
    if (url.pathname.startsWith('/api/')) {
      return jsonResponse({ error: 'Not found' }, { status: 404 });
    }
    if (!opts.staticDir) {
      return new Response('Dashboard static directory is not configured', {
        status: 404,
      });
    }
    return serveStaticFile(opts.staticDir, url.pathname);
  };
}

export function startWebDashboardServer(
  opts: {
    enabled?: boolean;
    host?: string;
    port?: number;
    staticDir?: string;
    getRoomBindings?: () => Record<string, RegisteredGroup>;
    enqueueMessageCheck?: (chatJid: string, groupFolder: string) => void;
    nudgeScheduler?: () => void;
  } = {},
): StartedWebDashboardServer | null {
  const enabled = opts.enabled ?? WEB_DASHBOARD.enabled;
  if (!enabled) return null;

  const host = opts.host ?? WEB_DASHBOARD.host;
  const port = opts.port ?? WEB_DASHBOARD.port;
  const staticDir = opts.staticDir ?? WEB_DASHBOARD.staticDir;
  const server = Bun.serve({
    hostname: host,
    port,
    fetch: createWebDashboardHandler({
      staticDir,
      getRoomBindings: opts.getRoomBindings,
      enqueueMessageCheck: opts.enqueueMessageCheck,
      nudgeScheduler: opts.nudgeScheduler,
    }),
  });
  const url = `http://${host}:${server.port}`;

  logger.info({ url, staticDir }, 'Web dashboard started');

  return {
    url,
    stop: () => server.stop(true),
  };
}
