import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';

import {
  AgentTokenStore,
  AppserviceIntent,
  TransactionHandler,
  validateBridgeMessage,
  validateBridgeTyping,
  validateProvisionRoom,
  validateRegisterAgent,
  validateRevokeAgent,
} from '@mosaicstack/appservice';
import type { AppserviceConfig, MatrixEvent } from '@mosaicstack/appservice';

export interface DaemonConfig extends AppserviceConfig {
  /** Bearer tokens accepted on /bridge/v1/* (one per agent-comms host daemon). */
  bridgeTokens: string[];
}

export interface DaemonRequest {
  method: string;
  /** URL path without query string. */
  path: string;
  searchParams: URLSearchParams;
  authorizationHeader?: string;
  body: unknown;
}

export interface DaemonResponse {
  status: number;
  body: Record<string, unknown>;
}

// Compare equal-length HMAC digests so neither content nor LENGTH of the
// stored secret is observable through timing.
const HMAC_KEY = randomBytes(32);
const digest = (value: string): Buffer => createHmac('sha256', HMAC_KEY).update(value).digest();

const safeEqual = (a: string, b: string): boolean => timingSafeEqual(digest(a), digest(b));

const TXN_PATH = /^\/_matrix\/app\/v1\/transactions\/([^/]+)$/;

/**
 * Resolved identity for an authenticated /bridge/v1/* caller. Host principals
 * (the agent-comms host daemons) are unrestricted; agent principals are scoped
 * to a single virtual user and may only act as themselves.
 */
export type BridgePrincipal = { kind: 'host' } | { kind: 'agent'; agentUserId: string } | null;

/**
 * HTTP-framework-agnostic request router for the mosaic-as daemon: the
 * Application Service transactions endpoint (Synapse-facing) plus the
 * internal bridge API v1 (agent-comms daemon-facing). main.ts binds this to
 * node:http; tests drive it directly.
 */
export class AppserviceDaemon {
  readonly intent: AppserviceIntent;
  private readonly transactions: TransactionHandler;
  private readonly agents: AgentTokenStore;

  constructor(
    private readonly cfg: DaemonConfig,
    fetchImpl?: typeof fetch,
    private readonly log: (line: string) => void = (line) => console.log(line),
  ) {
    this.intent = new AppserviceIntent(cfg, fetchImpl);
    this.agents = new AgentTokenStore(this.intent);
    this.transactions = new TransactionHandler({
      hsToken: cfg.hsToken,
      onEvent: (event) => this.onEvent(event),
      onError: (error, txnId) => this.log(`txn ${txnId} handler error: ${String(error)}`),
    });
  }

  /** v1: the daemon only observes; room logic lives in the agent-comms daemons. */
  private onEvent(event: MatrixEvent): void {
    if (event.type === 'm.room.message') {
      this.log(
        `event ${event.event_id ?? '?'} in ${event.room_id ?? '?'} from ${event.sender ?? '?'}`,
      );
    }
  }

  /** Resolve the calling principal, or null when unauthorized. Fail-closed:
   * host tokens win (timing-safe compare); otherwise a magt_* bearer is looked
   * up in the agent token store; anything else is rejected. */
  private async bridgeAuthorized(
    authorizationHeader: string | undefined,
  ): Promise<BridgePrincipal> {
    if (!authorizationHeader?.startsWith('Bearer ')) return null;
    const presented = authorizationHeader.slice('Bearer '.length);
    if (this.cfg.bridgeTokens.some((token) => safeEqual(presented, token))) {
      return { kind: 'host' };
    }
    const agentUserId = await this.agents.verifyToken(presented);
    if (agentUserId) return { kind: 'agent', agentUserId };
    return null;
  }

  async handle(req: DaemonRequest): Promise<DaemonResponse> {
    if (req.method === 'GET' && req.path === '/health') {
      return { status: 200, body: { ok: true } };
    }

    const txnMatch = req.method === 'PUT' ? TXN_PATH.exec(req.path) : null;
    if (txnMatch?.[1] !== undefined) {
      return this.transactions.handle(txnMatch[1], req.body, {
        authorizationHeader: req.authorizationHeader,
        accessTokenParam: req.searchParams.get('access_token') ?? undefined,
      });
    }

    if (req.path.startsWith('/bridge/v1/')) {
      const principal = await this.bridgeAuthorized(req.authorizationHeader);
      if (!principal) {
        return { status: 403, body: { errcode: 'M_FORBIDDEN', error: 'bad bridge token' } };
      }
      try {
        if (req.method === 'POST' && req.path === '/bridge/v1/agents') {
          if (principal.kind !== 'host') {
            return {
              status: 403,
              body: { errcode: 'M_FORBIDDEN', error: 'agents cannot register agents' },
            };
          }
          validateRegisterAgent(req.body);
          const { agentUserId, token } = await this.agents.register({
            alias: req.body.alias,
            host: req.body.host,
            displayName: req.body.display_name,
          });
          this.log(`registered agent ${agentUserId}`);
          return { status: 200, body: { agent_user_id: agentUserId, bridge_token: token } };
        }
        if (req.method === 'POST' && req.path === '/bridge/v1/agents/revoke') {
          if (principal.kind !== 'host') {
            return {
              status: 403,
              body: { errcode: 'M_FORBIDDEN', error: 'agents cannot revoke agents' },
            };
          }
          validateRevokeAgent(req.body);
          const revoked = await this.agents.revoke(req.body.agent_user_id);
          this.log(`revoked ${revoked} token(s) for ${req.body.agent_user_id}`);
          return { status: 200, body: { revoked } };
        }
        if (req.method === 'GET' && req.path === '/bridge/v1/agents') {
          if (principal.kind !== 'host') {
            return {
              status: 403,
              body: { errcode: 'M_FORBIDDEN', error: 'agents cannot list agents' },
            };
          }
          const agents = await this.agents.list();
          return { status: 200, body: { agents } };
        }
        if (req.method === 'POST' && req.path === '/bridge/v1/messages') {
          validateBridgeMessage(req.body);
          if (
            principal.kind === 'agent' &&
            this.intent.agentUserId(req.body.agent) !== principal.agentUserId
          ) {
            return {
              status: 403,
              body: { errcode: 'M_FORBIDDEN', error: 'token not scoped to this agent' },
            };
          }
          const eventId = await this.intent.sendAsAgent({
            roomId: req.body.room_id,
            agent: req.body.agent,
            body: req.body.body,
            threadRoot: req.body.thread_root,
            msgtype: req.body.msgtype,
            extraContent: req.body.extra_content,
          });
          return { status: 200, body: { event_id: eventId ?? null } };
        }
        if (req.method === 'POST' && req.path === '/bridge/v1/typing') {
          validateBridgeTyping(req.body);
          if (
            principal.kind === 'agent' &&
            this.intent.agentUserId(req.body.agent) !== principal.agentUserId
          ) {
            return {
              status: 403,
              body: { errcode: 'M_FORBIDDEN', error: 'token not scoped to this agent' },
            };
          }
          await this.intent.setTyping(req.body.room_id, req.body.agent, req.body.typing);
          return { status: 200, body: {} };
        }
        if (req.method === 'POST' && req.path === '/bridge/v1/provision/rooms') {
          validateProvisionRoom(req.body);
          const result = await this.intent.createRoom({
            name: req.body.name,
            alias: req.body.alias,
            topic: req.body.topic,
            invite: req.body.invite,
            spaceId: req.body.space_id,
          });
          this.log(
            `provisioned room ${result.roomId} (${req.body.name}) space_linked=${result.spaceLinked}`,
          );
          return {
            status: 200,
            body: {
              room_id: result.roomId,
              space_linked: result.spaceLinked,
              ...(result.spaceError ? { space_error: result.spaceError } : {}),
            },
          };
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        this.log(`bridge error ${req.method} ${req.path}: ${message}`);
        return { status: 400, body: { error: message } };
      }
      // Explicit: never fall out of the authenticated bridge block, so future
      // sub-paths cannot accidentally route around the auth guard above.
      return { status: 405, body: { error: 'unsupported bridge method/path' } };
    }

    return { status: 404, body: { error: 'not found' } };
  }
}
