import { DatabaseSync } from "node:sqlite";
import { emitEvent, type DewLinkEvent } from "../../events.ts";
import { sendWithAck } from "../../sendAck.ts";
import {
  approvePendingPairing as approveSharedPendingPairing,
  ensurePairingSchema,
  ensurePendingPairing as ensureSharedPendingPairing,
  pairingCounts,
  pairingStatus as sharedPairingStatus,
  unpairedBody,
  type PairingStatus,
} from "../shared/pairing.ts";

export type SignalEnvelope = {
  id?: string | number;
  timestamp?: number;
  source?: string;
  sourceNumber?: string;
  sourceName?: string;
  sourceUuid?: string;
  sender?: string;
  senderName?: string;
  groupInfo?: {
    groupId?: string;
    name?: string;
  };
  dataMessage?: SignalDataMessage;
  syncMessage?: {
    sentMessage?: SignalDataMessage & {
      destination?: string;
      timestamp?: number;
    };
  };
};

export type SignalDataMessage = {
  timestamp?: number;
  message?: string;
  body?: string;
  text?: string;
  expiresInSeconds?: number;
  attachments?: Array<{ id?: string; contentType?: string; filename?: string }>;
  groupInfo?: {
    groupId?: string;
    name?: string;
  };
  quote?: {
    id?: string | number;
    author?: string;
  };
};

export type SignalInboundEvent = {
  envelope?: SignalEnvelope;
  account?: string;
  jsonrpc?: string;
  method?: string;
  params?: { envelope?: SignalEnvelope; account?: string };
};

export type NormalizedSignalInbound = {
  convExternalId: string;
  convTitle: string;
  direction: "IN" | "OUT";
  body: string;
  sender: string;
  ts: number;
  providerMsgId: string;
  mediaType?: string;
  mediaRef?: string;
  replyToProviderMsgId?: string;
  isPrivate: boolean;
};

export type SignalTransport = {
  start(handler: (event: SignalInboundEvent) => Promise<void>): Promise<void>;
  stop?(): Promise<void> | void;
  sendMessage(recipient: string, text: string, options?: Record<string, unknown>): Promise<unknown>;
};

export type SignalAdapterStatus = {
  configured: boolean;
  inert: boolean;
  connected: boolean;
  account_id: string;
  pending_pairings: number;
  approved_senders: number;
  last_error: string | null;
};

type Emit = (ev: DewLinkEvent) => void;

const PAIRING = { prefix: "signal", module: "signal", channelLabel: "Signal" };

export type { PairingStatus };

function senderId(envelope: SignalEnvelope): string {
  return String(envelope.sourceNumber ?? envelope.sourceUuid ?? envelope.source ?? envelope.sender ?? "").trim();
}

function senderName(envelope: SignalEnvelope): string {
  return String(envelope.sourceName ?? envelope.senderName ?? senderId(envelope) ?? "Unknown Signal user");
}

function dataMessage(envelope: SignalEnvelope): { msg?: SignalDataMessage; direction: "IN" | "OUT"; peer?: string } {
  if (envelope.syncMessage?.sentMessage) {
    return { msg: envelope.syncMessage.sentMessage, direction: "OUT", peer: envelope.syncMessage.sentMessage.destination };
  }
  return { msg: envelope.dataMessage, direction: "IN" };
}

function groupInfo(envelope: SignalEnvelope, msg?: SignalDataMessage) {
  return msg?.groupInfo ?? envelope.groupInfo;
}

function firstAttachment(msg?: SignalDataMessage): { type?: string; ref?: string; label?: string } {
  const a = msg?.attachments?.[0];
  if (!a) return {};
  const ct = String(a.contentType ?? "").toLowerCase();
  const type = ct.startsWith("image/") ? "image"
    : ct.startsWith("video/") ? "video"
    : ct.startsWith("audio/") ? "audio"
    : "document";
  return { type, ref: a.id, label: a.filename ? `[Attachment] ${a.filename}` : "[Attachment]" };
}

export function signalConversationId(envelope: SignalEnvelope): string {
  const { msg, peer } = dataMessage(envelope);
  const group = groupInfo(envelope, msg);
  if (group?.groupId) return `group:${group.groupId}`;
  return peer || senderId(envelope);
}

export function signalProviderMessageId(conversationId: string, timestamp: string | number): string {
  return `sg:${conversationId}:${timestamp}`;
}

export function signalResultProviderMsgId(result: any, recipient: string): string | undefined {
  const id = result?.timestamp ?? result?.result?.timestamp ?? result?.messageId ?? result?.result?.messageId ?? result?.id ?? result?.result?.id;
  return id == null ? undefined : signalProviderMessageId(recipient, id);
}

export function normalizeSignalInbound(event: SignalInboundEvent): NormalizedSignalInbound | null {
  const envelope = event.envelope ?? event.params?.envelope;
  if (!envelope) return null;
  const { msg, direction, peer } = dataMessage(envelope);
  if (!msg) return null;
  const convExternalId = signalConversationId(envelope);
  if (!convExternalId) return null;
  const group = groupInfo(envelope, msg);
  const sender = direction === "OUT" ? "You" : senderName(envelope);
  const att = firstAttachment(msg);
  const body = String(msg.message ?? msg.body ?? msg.text ?? att.label ?? "[Message]");
  const ts = Number(msg.timestamp ?? envelope.timestamp ?? Date.now());
  const providerMsgId = signalProviderMessageId(convExternalId, ts);
  return {
    convExternalId,
    convTitle: group?.name ?? peer ?? senderName(envelope) ?? convExternalId,
    direction,
    body,
    sender,
    ts,
    providerMsgId,
    mediaType: att.type,
    mediaRef: att.ref,
    replyToProviderMsgId: msg.quote?.id == null ? undefined : signalProviderMessageId(convExternalId, msg.quote.id),
    isPrivate: !group?.groupId,
  };
}

export function ensureSignalSchema(db: DatabaseSync): void {
  ensurePairingSchema(db, PAIRING);
}

export function signalPairingStatus(db: DatabaseSync, senderId: string): PairingStatus {
  return sharedPairingStatus(db, PAIRING, senderId);
}

export function ensureSignalPendingPairing(db: DatabaseSync, a: {
  senderId: string;
  senderName: string;
  conversationExternalId: string;
  now?: number;
}) {
  return ensureSharedPendingPairing(db, PAIRING, a);
}

export function approveSignalPendingPairing(db: DatabaseSync, senderId: string, approvedBy = "Roki", now = Date.now()): boolean {
  return approveSharedPendingPairing(db, PAIRING, senderId, approvedBy, now);
}

function retryAfterSeconds(error: unknown): number | null {
  const err = error as any;
  const n = err?.retryAfter ?? err?.retry_after ?? err?.response?.headers?.["retry-after"];
  const parsed = Number(n);
  if (Number.isFinite(parsed) && parsed > 0) return parsed;
  const text = String(err?.message ?? "");
  const match = text.match(/retry[-_ ]after\D+(\d+)/i);
  return match ? Number(match[1]) : null;
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export class SignalRateLimiter {
  private nextAt = 0;
  private chain: Promise<unknown> = Promise.resolve();
  private readonly minIntervalMs: number;
  private readonly sleepMs: (ms: number) => Promise<void>;

  constructor(opts: { minIntervalMs?: number; sleepMs?: (ms: number) => Promise<void> } = {}) {
    this.minIntervalMs = opts.minIntervalMs ?? 1000;
    this.sleepMs = opts.sleepMs ?? sleep;
  }

  run<T>(fn: () => Promise<T>, opts: { maxRetries?: number } = {}): Promise<T> {
    const task = this.chain.then(() => this.runOnce(fn, opts));
    this.chain = task.catch(() => undefined);
    return task;
  }

  private async runOnce<T>(fn: () => Promise<T>, opts: { maxRetries?: number }): Promise<T> {
    const maxRetries = opts.maxRetries ?? 1;
    let attempt = 0;
    for (;;) {
      const now = Date.now();
      if (this.nextAt > now) await this.sleepMs(this.nextAt - now);
      this.nextAt = Date.now() + this.minIntervalMs;
      try {
        return await fn();
      } catch (e) {
        const retryAfter = retryAfterSeconds(e);
        if (!retryAfter || attempt >= maxRetries) throw e;
        attempt++;
        await this.sleepMs(retryAfter * 1000);
      }
    }
  }
}

export class SignalChannelAdapter {
  private connected = false;
  private lastError: string | null = null;
  private readonly limiter: SignalRateLimiter;
  private readonly opts: {
    signalCliUrl: string;
    accountId?: string;
    transport?: SignalTransport;
    db?: DatabaseSync;
    emit?: Emit;
    sleepMs?: (ms: number) => Promise<void>;
    minSendIntervalMs?: number;
  };

  constructor(opts: {
    signalCliUrl: string;
    accountId?: string;
    transport?: SignalTransport;
    db?: DatabaseSync;
    emit?: Emit;
    sleepMs?: (ms: number) => Promise<void>;
    minSendIntervalMs?: number;
  }) {
    this.opts = opts;
    this.limiter = new SignalRateLimiter({ minIntervalMs: opts.minSendIntervalMs, sleepMs: opts.sleepMs });
  }

  status(): SignalAdapterStatus {
    const db = this.opts.db;
    const counts = db ? pairingCounts(db, PAIRING) : { pending: 0, approved: 0 };
    return {
      configured: !!this.opts.signalCliUrl.trim(),
      inert: !this.opts.signalCliUrl.trim(),
      connected: this.connected,
      account_id: this.opts.accountId ?? "default",
      pending_pairings: counts.pending,
      approved_senders: counts.approved,
      last_error: this.lastError,
    };
  }

  async connect(): Promise<SignalAdapterStatus> {
    const emit = this.opts.emit ?? emitEvent;
    if (!this.opts.signalCliUrl.trim()) {
      this.connected = false;
      emit({ type: "status", state: "logged_out" });
      return this.status();
    }
    if (!this.opts.transport) throw new Error("signal transport is required");
    emit({ type: "status", state: "connecting" });
    try {
      await this.opts.transport.start((event) => this.handleEvent(event));
      this.connected = true;
      emit({ type: "status", state: "open", user: this.opts.accountId ?? "default" });
    } catch (e: any) {
      this.connected = false;
      this.lastError = e?.message ?? String(e);
      emit({ type: "status", state: "close" });
      emit({ type: "log", level: "error", msg: `[signal] connect failed: ${this.lastError}` });
    }
    return this.status();
  }

  async handleEvent(event: SignalInboundEvent): Promise<NormalizedSignalInbound | null> {
    const normalized = normalizeSignalInbound(event);
    if (!normalized) return null;
    let body = normalized.body;
    const envelope = event.envelope ?? event.params?.envelope;
    const sid = envelope ? senderId(envelope) : "";
    if (normalized.direction === "IN" && normalized.isPrivate && sid && this.opts.db) {
      const status = signalPairingStatus(this.opts.db, sid);
      if (status !== "approved") {
        const pending = ensureSignalPendingPairing(this.opts.db, {
          senderId: sid,
          senderName: normalized.sender,
          conversationExternalId: normalized.convExternalId,
        });
        body = unpairedBody("Signal", body, pending);
      }
    }
    (this.opts.emit ?? emitEvent)({
      type: "message",
      convExternalId: normalized.convExternalId,
      convTitle: normalized.convTitle,
      direction: normalized.direction,
      body,
      sender: normalized.sender,
      ts: normalized.ts,
      providerMsgId: normalized.providerMsgId,
      historical: false,
      mediaType: normalized.mediaType,
      mediaPath: normalized.mediaRef,
    });
    return { ...normalized, body };
  }

  async sendApprovedDraft(cmd: { draftId?: unknown; recipient: string; text: string }) {
    if (!this.opts.transport) throw new Error("signal transport is required");
    return sendWithAck(
      { draftId: cmd.draftId, chatId: cmd.recipient, text: cmd.text },
      () => this.limiter.run(() => this.opts.transport!.sendMessage(cmd.recipient, cmd.text)),
      (result) => signalResultProviderMsgId(result, cmd.recipient),
      this.opts.emit ?? emitEvent,
    );
  }
}

export function signalStatusFromDb(db: DatabaseSync, env: NodeJS.ProcessEnv = process.env): SignalAdapterStatus {
  ensureSignalSchema(db);
  db.exec(`CREATE TABLE IF NOT EXISTS engine_settings (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL DEFAULT 0);`);
  const row = db.prepare(`SELECT value FROM engine_settings WHERE key='signal_cli_url' LIMIT 1`).get() as
    | { value?: string | null }
    | undefined;
  const url = String(row?.value ?? env.SIGNAL_CLI_URL ?? "").trim();
  const counts = pairingCounts(db, PAIRING);
  return {
    configured: !!url,
    inert: !url,
    connected: false,
    account_id: env.SIGNAL_ACCOUNT_ID || "default",
    pending_pairings: counts.pending,
    approved_senders: counts.approved,
    last_error: null,
  };
}
