import { createHmac, randomUUID, timingSafeEqual } from 'crypto';
import type { IncomingMessage, ServerResponse } from 'http';
import type { WebhookConfig } from '../config/schema.js';
import type { ChannelAdapter, ChannelStatus, InboundMessage, OutboundMessage } from '../channels/types.js';
import { auditLogger } from '../audit/index.js';
import { RequestBodyTooLargeError, readRequestBody } from '../utils/httpBody.js';

/** Minimal interface for the parts of ChannelRegistry we need. */
interface ChannelLookup {
  get(name: string): { send(peerId: string, message: OutboundMessage): Promise<void> } | undefined;
}

type DeliveryMode = 'shared_session' | 'isolated_job';

/** Verify HMAC-SHA256 signature from the X-Webhook-Signature header. */
function verifyHmac(body: string, secret: string, signature: string): boolean {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const sig = signature.startsWith('sha256=') ? signature.slice(7) : signature;

  if (expected.length !== sig.length) {return false;}

  try {
    return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'));
  } catch {
    return false;
  }
}

/**
 * Render a message template with {{body}} and {{json.field}} placeholders.
 * - {{body}} is replaced with the raw request body.
 * - {{json.field}} accesses a top-level field from the parsed JSON body.
 */
function renderTemplate(template: string, body: string): string {
  let result = template.replace(/\{\{body\}\}/g, body);

  // Replace {{json.field}} placeholders
  let parsed: Record<string, unknown> | undefined;
  result = result.replace(/\{\{json\.([^}]+)\}\}/g, (_match, field: string) => {
    if (!parsed) {
      try {
        parsed = JSON.parse(body) as Record<string, unknown>;
      } catch {
        return '';
      }
    }
    const value = parsed[field];
    if (value === undefined || value === null) {return '';}
    return typeof value === 'string' ? value : JSON.stringify(value);
  });

  return result;
}

export class WebhookHandler implements ChannelAdapter {
  private static readonly DEFAULT_MAX_REQUEST_BODY_BYTES = 1_048_576; // 1 MiB
  readonly name = 'webhook';
  private _status: ChannelStatus = 'disconnected';
  private messageHandler?: (msg: InboundMessage) => void;
  private webhooks: Map<string, WebhookConfig> = new Map();

  constructor(
    private readonly webhookConfigs: WebhookConfig[],
    private readonly channelLookup: ChannelLookup,
    private readonly deliveryMode: DeliveryMode = 'shared_session',
    private readonly maxRequestBodyBytes: number = WebhookHandler.DEFAULT_MAX_REQUEST_BODY_BYTES,
  ) {
    for (const webhook of webhookConfigs) {
      this.webhooks.set(webhook.name, webhook);
    }
  }

  get status(): ChannelStatus {
    return this._status;
  }

  async connect(): Promise<void> {
    this._status = 'connected';

    const enabledCount = this.webhookConfigs.filter(w => w.enabled).length;
    if (enabledCount > 0) {
      console.log(`WebhookHandler: ${enabledCount} webhook(s) registered`);
      auditLogger?.systemStart('WebhookHandler', { webhooks_enabled: enabledCount });
    }
  }

  async disconnect(): Promise<void> {
    this._status = 'disconnected';
    auditLogger?.systemStop('WebhookHandler');
  }

  async send(peerId: string, message: OutboundMessage): Promise<void> {
    // peerId is the webhook name — look up its output config
    const webhook = this.webhooks.get(peerId);
    if (!webhook) {
      console.warn(`No webhook found for '${peerId}'`);
      return;
    }

    const outputAdapter = this.channelLookup.get(webhook.output.channel);
    if (!outputAdapter) {
      console.warn(`Output channel '${webhook.output.channel}' not found for webhook '${peerId}'`);
      return;
    }

    await outputAdapter.send(webhook.output.peer, message);
  }

  onMessage(handler: (msg: InboundMessage) => void): void {
    this.messageHandler = handler;
  }

  /**
   * Handle an incoming HTTP webhook request.
   * Returns true if the webhook was found and processed, false otherwise.
   */
  async handleRequest(webhookName: string, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
    const webhook = this.webhooks.get(webhookName);
    if (!webhook) {
      auditLogger?.webhookNotFound(webhookName);
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Unknown webhook' }));
      return false;
    }

    if (!webhook.enabled) {
      auditLogger?.webhookDenied(webhookName, 'Webhook disabled');
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Webhook disabled' }));
      return false;
    }

    let body = '';
    try {
      body = await readRequestBody(req, { maxBytes: this.maxRequestBodyBytes });
    } catch (err) {
      if (err instanceof RequestBodyTooLargeError) {
        auditLogger?.webhookDenied(webhookName, `Payload too large (>${this.maxRequestBodyBytes} bytes)`);
        res.writeHead(413, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Payload too large' }));
        return false;
      }
      auditLogger?.webhookDenied(webhookName, err instanceof Error ? err.message : 'Failed to read request body');
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid request body' }));
      return false;
    }

    // Verify HMAC if secret is configured
    const signatureVerified = !webhook.secret;
    if (webhook.secret) {
      const signature = req.headers['x-webhook-signature'] as string | undefined;
      const verified = signature ? verifyHmac(body, webhook.secret, signature) : false;
      if (!verified) {
        auditLogger?.webhookDenied(webhookName, 'Invalid HMAC signature');
        res.writeHead(401, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid signature' }));
        return false;
      }
    }

    // Render message template
    const text = renderTemplate(webhook.message, body);
    const runId = `run-${randomUUID()}`;
    const senderId = this.deliveryMode === 'isolated_job' ? `${webhookName}:${runId}` : webhookName;

    const msg: InboundMessage = {
      id: `webhook-${webhookName}-${Date.now()}`,
      channel: 'webhook',
      senderId,
      senderName: `webhook:${webhookName}`,
      text,
      timestamp: Date.now(),
      metadata: {
        webhookName,
        body,
        deliveryMode: this.deliveryMode,
        runId,
        replyPeerId: webhookName,
      },
    };

    auditLogger?.webhookReceive({
      webhook_name: webhookName,
      body,
      signature_verified: signatureVerified,
      output_channel: webhook.output.channel,
      output_peer: webhook.output.peer,
    });

    this.messageHandler?.(msg);

    res.writeHead(202, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ accepted: true }));
    return true;
  }

  /** Get list of all webhook names (enabled and disabled). */
  getWebhookNames(): string[] {
    return Array.from(this.webhooks.keys());
  }
}

// Export helpers for testing
export { verifyHmac as _verifyHmac, renderTemplate as _renderTemplate };
