import type { GatewayRequest, OutboundMessage } from '../protocol.js';
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
import type { MetricsSnapshot, EventEntry, ActiveRequestInfo } from '../metrics.js';
import type { ServiceInfo } from './services.js';
import type { NodeLocation, NodeStatus, NodePushToken } from './node.js';
import type { SessionAnalyticsSnapshot } from '../../session/index.js';
import type { LocalBackendAction, LocalBackendControlResult, LocalBackendStatus } from './localBackends.js';
import type {
  DockerDependencyAction,
  DockerDependencyControlResult,
  DockerDependencyStatus,
} from './dockerDependencies.js';
import type {
  ObservabilitySource,
  ObservabilitySeriesQuery,
  ObservabilitySeriesSnapshot,
  ServiceLogQuery,
  ServiceLogSnapshot,
} from './observability.js';

/** Per-session token usage report returned by system.tokenUsage. */
export interface TokenUsageEntry {
  sessionId: string;
  primary: { inputTokens: number; outputTokens: number; calls: number };
  delegation: Record<string, { inputTokens: number; outputTokens: number; calls: number }>;
  total: { inputTokens: number; outputTokens: number; calls: number; estimatedCost: number };
}

/** Per-session context budget report returned by system.contextUsage. */
export interface ContextUsageEntry {
  sessionId: string;
  budget: {
    estimatedTokens: number;
    contextWindow: number;
    remainingTokens: number;
    usagePct: number;
    thresholdPct: number;
    thresholdTokens: number;
    shouldCompact: boolean;
  };
}

export interface PresenceEntry {
  channel: string;
  senderId: string;
  senderName?: string;
  firstSeenAt: number;
  lastSeenAt: number;
  messageCount: number;
  status: 'online' | 'offline';
}

export interface NodeLocationEntry {
  nodeId: string;
  role: string;
  connectionId: string;
  location: NodeLocation;
}

export interface NodeEntry {
  connectionId: string;
  nodeId: string;
  role: string;
  identity?: string;
  protocolVersion: number;
  capabilities: string[];
  registeredAt: number;
  location?: NodeLocation;
  status?: NodeStatus;
  push?: NodePushTokenSummary;
}

export interface NodePushTokenSummary {
  provider: NodePushToken['provider'];
  tokenPreview: string;
  topic?: string;
  environment?: NodePushToken['environment'];
  registeredAt: number;
}

export interface SystemHandlerDeps {
  startTime: number;
  version: string;
  getSessionCount: () => number;
  getToolCount: () => number;
  getConnectionCount: () => number;
  /** Optional callback to trigger a graceful restart. If not provided, system.restart returns an error. */
  restart?: () => Promise<void>;
  getChannels?: () => Array<{ name: string; status: string; error?: string; lastErrorAt?: number }>;
  getUsage?: () => { totalSessions: number; activeConnections: number };
  /** Optional callback to retrieve per-session token usage data. */
  getTokenUsage?: () => TokenUsageEntry[];
  /** Optional callback to retrieve per-session context budget data. */
  getContextUsage?: () => ContextUsageEntry[];
  /** Optional callback to retrieve aggregated metrics snapshot. */
  getMetrics?: () => MetricsSnapshot;
  /** Optional callback to retrieve session analytics. */
  getSessionAnalytics?: (opts?: { days?: number; topLimit?: number }) => SessionAnalyticsSnapshot;
  /** Optional callback to retrieve recent events. */
  getEvents?: (opts?: { level?: string; limit?: number }) => EventEntry[];
  /** Optional callback to retrieve active requests. */
  getActiveRequests?: () => ActiveRequestInfo[];
  /** Optional callback to retrieve all services (channels + automation). */
  getServices?: () => ServiceInfo[];
  /** Optional callback to retrieve tracked sender presence. */
  getPresence?: (opts?: { channel?: string; status?: 'online' | 'offline'; limit?: number }) => PresenceEntry[];
  /** Optional callback to retrieve latest node location data. */
  getNodeLocations?: (opts?: { role?: string; nodeId?: string; limit?: number }) => NodeLocationEntry[];
  /** Optional callback to retrieve registered node connection snapshots. */
  getNodes?: (opts?: { role?: string; platform?: string; limit?: number }) => NodeEntry[];
  /** Optional callback to retrieve provider model catalogs. */
  getModelCatalog?: (opts?: { provider?: string; forceRefresh?: boolean }) => Promise<Array<{
    provider: string;
    models: string[];
    source: 'api' | 'config' | 'unavailable';
    error?: string;
    fetchedAt: number;
  }>>;
  /** Optional callback to retrieve local backend daemon statuses. */
  getLocalBackends?: () => Promise<LocalBackendStatus[]> | LocalBackendStatus[];
  /** Optional callback to control local backend daemons. */
  controlLocalBackend?: (backend: string, action: string) => Promise<LocalBackendControlResult>;
  /** Optional callback to retrieve docker-compose dependency statuses. */
  getDockerDependencies?: () => Promise<DockerDependencyStatus[]> | DockerDependencyStatus[];
  /** Optional callback to control docker-compose dependencies. */
  controlDockerDependency?: (dependency: string, action: string) => Promise<DockerDependencyControlResult>;
  /** Optional callback to retrieve observability-capable service sources. */
  getObservabilitySources?: () => Promise<ObservabilitySource[]> | ObservabilitySource[];
  /** Optional callback to retrieve sampled observability series for services. */
  getObservabilitySeries?: (opts?: ObservabilitySeriesQuery) => Promise<ObservabilitySeriesSnapshot> | ObservabilitySeriesSnapshot;
  /** Optional callback to retrieve recent logs for a service source. */
  getServiceLogs?: (opts: ServiceLogQuery) => Promise<ServiceLogSnapshot> | ServiceLogSnapshot;
}

function normalizeErrorMessage(error: unknown): string {
  if (error instanceof Error && error.message.trim().length > 0) {
    return error.message;
  }
  if (typeof error === 'string' && error.trim().length > 0) {
    return error;
  }
  return 'Unknown error';
}

export function createSystemHandlers(deps: SystemHandlerDeps) {
  return {
    'system.health': async (request: GatewayRequest): Promise<OutboundMessage> => {
      return makeResponse(request.id, {
        status: 'ok',
        uptime: Math.floor((Date.now() - deps.startTime) / 1000),
        version: deps.version,
        sessions: deps.getSessionCount(),
        tools: deps.getToolCount(),
        connections: deps.getConnectionCount(),
      });
    },

    'system.restart': async (request: GatewayRequest): Promise<OutboundMessage> => {
      const restart = deps.restart;
      if (!restart) {
        return makeError(request.id, ErrorCode.InternalError, 'Restart not available in this environment');
      }

      // Send response before initiating restart (client receives confirmation)
      const response = makeResponse(request.id, { restarting: true });

      // Schedule restart after response is sent (next tick)
      queueMicrotask(() => {
        restart().catch((err) => {
          console.error('Restart failed:', err);
        });
      });

      return response;
    },

    'system.channels': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getChannels) {
        return makeResponse(request.id, { channels: [] });
      }
      return makeResponse(request.id, { channels: deps.getChannels() });
    },

    'system.services': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getServices) {
        return makeResponse(request.id, { services: [] });
      }
      return makeResponse(request.id, { services: deps.getServices() });
    },

    'system.presence': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getPresence) {
        return makeResponse(request.id, { presence: [], summary: { total: 0, online: 0, offline: 0 } });
      }

      const params = request.params as { channel?: string; status?: 'online' | 'offline'; limit?: number } | undefined;
      const presence = deps.getPresence({
        channel: params?.channel,
        status: params?.status,
        limit: params?.limit,
      });
      const online = presence.filter((entry) => entry.status === 'online').length;
      return makeResponse(request.id, {
        presence,
        summary: {
          total: presence.length,
          online,
          offline: presence.length - online,
        },
      });
    },

    'system.location': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getNodeLocations) {
        return makeResponse(request.id, { locations: [], summary: { total: 0 } });
      }

      const params = request.params as { role?: string; nodeId?: string; limit?: number } | undefined;
      const locations = deps.getNodeLocations({
        role: params?.role,
        nodeId: params?.nodeId,
        limit: params?.limit,
      });
      return makeResponse(request.id, {
        locations,
        summary: {
          total: locations.length,
        },
      });
    },

    'system.nodes': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getNodes) {
        return makeResponse(request.id, { nodes: [], summary: { total: 0 } });
      }

      const params = request.params as { role?: string; platform?: string; limit?: number } | undefined;
      const nodes = deps.getNodes({
        role: params?.role,
        platform: params?.platform,
        limit: params?.limit,
      });
      return makeResponse(request.id, {
        nodes,
        summary: {
          total: nodes.length,
        },
      });
    },

    'system.usage': async (request: GatewayRequest): Promise<OutboundMessage> => {
      const uptime = Math.floor((Date.now() - deps.startTime) / 1000);
      const usage = deps.getUsage?.() ?? { totalSessions: 0, activeConnections: 0 };
      return makeResponse(request.id, {
        uptime,
        ...usage,
        tools: deps.getToolCount(),
      });
    },

    'system.tokenUsage': async (request: GatewayRequest): Promise<OutboundMessage> => {
      const sessions = deps.getTokenUsage?.() ?? [];
      return makeResponse(request.id, { sessions });
    },

    'system.contextUsage': async (request: GatewayRequest): Promise<OutboundMessage> => {
      const sessions = deps.getContextUsage?.() ?? [];
      return makeResponse(request.id, { sessions });
    },

    'system.metrics': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getMetrics) {
        return makeResponse(request.id, {});
      }
      return makeResponse(request.id, deps.getMetrics());
    },

    'system.sessionAnalytics': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getSessionAnalytics) {
        return makeResponse(request.id, {
          daily: [],
          topSessions: [],
          topTools: [],
          topTopics: [],
          averageMessagesPerSession: 0,
          totalSessions: 0,
          totalMessages: 0,
        });
      }
      const params = request.params as { days?: number; topLimit?: number } | undefined;
      const snapshot = deps.getSessionAnalytics({
        days: params?.days,
        topLimit: params?.topLimit,
      });
      return makeResponse(request.id, snapshot);
    },

    'system.events': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getEvents) {
        return makeResponse(request.id, { events: [] });
      }
      const params = request.params as { level?: string; limit?: number } | undefined;
      const events = deps.getEvents({ level: params?.level, limit: params?.limit });
      return makeResponse(request.id, { events });
    },

    'system.activeRequests': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getActiveRequests) {
        return makeResponse(request.id, { requests: [] });
      }
      return makeResponse(request.id, { requests: deps.getActiveRequests() });
    },

    'system.modelCatalog': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getModelCatalog) {
        return makeResponse(request.id, { providers: [] });
      }
      const params = request.params as { provider?: string; forceRefresh?: boolean } | undefined;
      const providers = await deps.getModelCatalog({
        provider: params?.provider,
        forceRefresh: params?.forceRefresh === true,
      });
      return makeResponse(request.id, { providers });
    },

    'system.localBackends': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getLocalBackends) {
        return makeResponse(request.id, { backends: [] });
      }
      try {
        const backends = await deps.getLocalBackends();
        return makeResponse(request.id, { backends });
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Failed to load local backends: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.localBackendControl': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.controlLocalBackend) {
        return makeError(request.id, ErrorCode.InternalError, 'Local backend control is not available in this environment');
      }
      const params = request.params as { backend?: string; action?: LocalBackendAction } | undefined;
      if (!params?.backend || typeof params.backend !== 'string') {
        return makeError(request.id, ErrorCode.InvalidRequest, 'backend is required');
      }
      if (!params?.action || typeof params.action !== 'string') {
        return makeError(request.id, ErrorCode.InvalidRequest, 'action is required');
      }
      if (!['start', 'restart', 'stop', 'update'].includes(params.action)) {
        return makeError(request.id, ErrorCode.InvalidRequest, 'action must be one of: start, restart, stop, update');
      }

      try {
        const result = await deps.controlLocalBackend(params.backend, params.action);
        return makeResponse(request.id, result);
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Local backend control failed: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.dockerDependencies': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getDockerDependencies) {
        return makeResponse(request.id, { dependencies: [] });
      }
      try {
        const dependencies = await deps.getDockerDependencies();
        return makeResponse(request.id, { dependencies });
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Failed to load docker dependencies: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.dockerDependencyControl': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.controlDockerDependency) {
        return makeError(request.id, ErrorCode.InternalError, 'Docker dependency control is not available in this environment');
      }
      const params = request.params as { dependency?: string; action?: DockerDependencyAction } | undefined;
      if (!params?.dependency || typeof params.dependency !== 'string') {
        return makeError(request.id, ErrorCode.InvalidRequest, 'dependency is required');
      }
      if (!params?.action || typeof params.action !== 'string') {
        return makeError(request.id, ErrorCode.InvalidRequest, 'action is required');
      }
      if (!['start', 'restart', 'stop', 'update'].includes(params.action)) {
        return makeError(request.id, ErrorCode.InvalidRequest, 'action must be one of: start, restart, stop, update');
      }

      try {
        const result = await deps.controlDockerDependency(params.dependency, params.action);
        return makeResponse(request.id, result);
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Docker dependency control failed: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.observabilitySources': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getObservabilitySources) {
        return makeResponse(request.id, { sources: [] });
      }
      try {
        const sources = await deps.getObservabilitySources();
        return makeResponse(request.id, { sources });
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Failed to load observability sources: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.observabilitySeries': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getObservabilitySeries) {
        return makeResponse(request.id, {
          generatedAt: Date.now(),
          windowMinutes: 60,
          bucketSeconds: 30,
          series: [],
        });
      }

      const params = request.params as { windowMinutes?: number; bucketSeconds?: number; sourceIds?: unknown } | undefined;
      if (
        params?.sourceIds !== undefined
        && (!Array.isArray(params.sourceIds) || params.sourceIds.some((entry) => typeof entry !== 'string'))
      ) {
        return makeError(request.id, ErrorCode.InvalidRequest, 'sourceIds must be an array of strings');
      }

      try {
        const snapshot = await deps.getObservabilitySeries({
          windowMinutes: params?.windowMinutes,
          bucketSeconds: params?.bucketSeconds,
          sourceIds: params?.sourceIds as string[] | undefined,
        });
        return makeResponse(request.id, snapshot);
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Failed to load observability series: ${normalizeErrorMessage(error)}`);
      }
    },

    'system.serviceLogs': async (request: GatewayRequest): Promise<OutboundMessage> => {
      if (!deps.getServiceLogs) {
        return makeError(request.id, ErrorCode.InternalError, 'Service logs are not available in this environment');
      }

      const params = request.params as { sourceId?: string; lines?: number; sinceSeconds?: number } | undefined;
      if (!params?.sourceId || typeof params.sourceId !== 'string' || params.sourceId.trim().length === 0) {
        return makeError(request.id, ErrorCode.InvalidRequest, 'sourceId is required');
      }

      try {
        const logs = await deps.getServiceLogs({
          sourceId: params.sourceId,
          lines: params.lines,
          sinceSeconds: params.sinceSeconds,
        });
        return makeResponse(request.id, logs);
      } catch (error) {
        return makeError(request.id, ErrorCode.InternalError, `Failed to load service logs: ${normalizeErrorMessage(error)}`);
      }
    },
  };
}
