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

export type TelegramChatType = "private" | "group" | "supergroup" | "channel";

export type TelegramUser = {
  id: number | string;
  username?: string;
  first_name?: string;
  last_name?: string;
  is_bot?: boolean;
};

export type TelegramMessage = {
  message_id: number;
  date?: number;
  chat: {
    id: number | string;
    type?: TelegramChatType | string;
    title?: string;
    username?: string;
    first_name?: string;
    last_name?: string;
  };
  from?: TelegramUser;
  message_thread_id?: number;
  text?: string;
  caption?: string;
  photo?: unknown[];
  document?: { file_id?: string; file_name?: string; mime_type?: string };
  video?: { file_id?: string; mime_type?: string };
  voice?: { file_id?: string; mime_type?: string };
  audio?: { file_id?: string; mime_type?: string };
  sticker?: { file_id?: string; emoji?: string };
  reply_to_message?: { message_id?: number };
};

export type TelegramUpdate = {
  update_id?: number;
  message?: TelegramMessage;
  edited_message?: TelegramMessage;
};

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

export type TelegramTransport = {
  start(handler: (update: TelegramUpdate) => Promise<void>): Promise<void>;
  stop?(): Promise<void> | void;
  sendMessage(chatId: string, text: string, options?: Record<string, unknown>): Promise<unknown>;
  editMessageText?(chatId: string, messageId: string | number, text: string, options?: Record<string, unknown>): Promise<unknown>;
  setMessageReaction?(chatId: string, messageId: string | number, emoji: string): Promise<unknown>;
  getMe?(): Promise<{ username?: string; first_name?: string; id?: number | string }>;
};

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

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

const CHANNEL = "TELEGRAM";
const PAIRING = { prefix: "telegram", module: "telegram", channelLabel: "Telegram" };

export { pairingCode };
export type { PairingStatus };

function userDisplayName(user?: TelegramUser): string {
  if (!user) return "Unknown Telegram user";
  const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
  return name || (user.username ? `@${user.username}` : String(user.id));
}

function chatTitle(message: TelegramMessage, sender: string): string {
  return message.chat.title || [message.chat.first_name, message.chat.last_name].filter(Boolean).join(" ").trim() ||
    message.chat.username && `@${message.chat.username}` || sender;
}

function media(message: TelegramMessage): { type?: string; ref?: string; label?: string } {
  if (message.photo) return { type: "image", ref: "telegram:photo", label: "[Photo]" };
  if (message.document) return { type: "document", ref: message.document.file_id, label: `[Document] ${message.document.file_name ?? ""}`.trim() };
  if (message.video) return { type: "video", ref: message.video.file_id, label: "[Video]" };
  if (message.voice) return { type: "audio", ref: message.voice.file_id, label: "[Voice message]" };
  if (message.audio) return { type: "audio", ref: message.audio.file_id, label: "[Audio]" };
  if (message.sticker) return { type: "sticker", ref: message.sticker.file_id, label: message.sticker.emoji || "[Sticker]" };
  return {};
}

export function telegramSessionKey(chatId: string | number, threadId?: string | number | null): string {
  return threadId == null || String(threadId).trim() === "" ? String(chatId) : `${chatId}:${threadId}`;
}

export function telegramProviderMessageId(chatId: string | number, messageId: string | number): string {
  return `tg:${chatId}:${messageId}`;
}

export function normalizeTelegramInbound(update: TelegramUpdate): NormalizedTelegramInbound | null {
  const message = update.message ?? update.edited_message;
  if (!message?.chat?.id || message.message_id == null) return null;
  const chatId = String(message.chat.id);
  const threadId = message.message_thread_id == null ? null : String(message.message_thread_id);
  const sender = userDisplayName(message.from);
  const m = media(message);
  const rawBody = message.text ?? message.caption ?? m.label ?? "[Message]";
  const edited = update.edited_message ? " (edited)" : "";
  return {
    convExternalId: telegramSessionKey(chatId, threadId),
    convTitle: chatTitle(message, sender),
    direction: "IN",
    body: `${rawBody}${edited}`,
    sender,
    ts: (Number(message.date) || Math.floor(Date.now() / 1000)) * 1000,
    providerMsgId: telegramProviderMessageId(chatId, message.message_id),
    mediaType: m.type,
    mediaRef: m.ref,
    replyToProviderMsgId: message.reply_to_message?.message_id == null
      ? undefined
      : telegramProviderMessageId(chatId, message.reply_to_message.message_id),
    threadId,
    isPrivate: (message.chat.type ?? "private") === "private",
  };
}

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

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

export function ensurePendingPairing(db: DatabaseSync, a: {
  senderId: string;
  senderName: string;
  conversationExternalId: string;
  now?: number;
}): { code: string; decision_id: string | null; created: boolean } {
  return ensureSharedPendingPairing(db, PAIRING, a);
}

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

function unpairedBody(body: string, pairing: { code: string; decision_id: string | null }): string {
  return sharedUnpairedBody("Telegram", body, pairing);
}

function retryAfterSeconds(error: unknown): number | null {
  const err = error as any;
  const n = err?.parameters?.retry_after ?? err?.retry_after ?? err?.response?.parameters?.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 async function withTelegramRateLimit<T>(
  fn: () => Promise<T>,
  opts: { sleepMs?: (ms: number) => Promise<void>; maxRetries?: number } = {},
): Promise<T> {
  const sleeper = opts.sleepMs ?? sleep;
  const maxRetries = opts.maxRetries ?? 1;
  let attempt = 0;
  for (;;) {
    try {
      return await fn();
    } catch (e) {
      const retryAfter = retryAfterSeconds(e);
      if (!retryAfter || attempt >= maxRetries) throw e;
      attempt++;
      await sleeper(retryAfter * 1000);
    }
  }
}

export class TelegramChannelAdapter {
  private connected = false;
  private username: string | null = null;
  private lastError: string | null = null;
  private readonly opts: {
    token: string;
    accountId?: string;
    transport?: TelegramTransport;
    db?: DatabaseSync;
    emit?: Emit;
    sleepMs?: (ms: number) => Promise<void>;
  };

  constructor(opts: {
    token: string;
    accountId?: string;
    transport?: TelegramTransport;
    db?: DatabaseSync;
    emit?: Emit;
    sleepMs?: (ms: number) => Promise<void>;
  }) {
    this.opts = opts;
  }

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

  async connect(): Promise<TelegramAdapterStatus> {
    const emit = this.opts.emit ?? emitEvent;
    if (!this.opts.token) {
      this.connected = false;
      emit({ type: "status", state: "logged_out" });
      return this.status();
    }
    if (!this.opts.transport) throw new Error("telegram transport is required");
    emit({ type: "status", state: "connecting" });
    try {
      const me = await this.opts.transport.getMe?.();
      this.username = me?.username ? `@${me.username}` : me?.first_name ?? null;
      await this.opts.transport.start((update) => this.handleUpdate(update));
      this.connected = true;
      emit({ type: "status", state: "open", user: this.username });
    } catch (e: any) {
      this.connected = false;
      this.lastError = e?.message ?? String(e);
      emit({ type: "status", state: "close" });
      emit({ type: "log", level: "error", msg: `[telegram] connect failed: ${this.lastError}` });
    }
    return this.status();
  }

  async handleUpdate(update: TelegramUpdate): Promise<NormalizedTelegramInbound | null> {
    const normalized = normalizeTelegramInbound(update);
    if (!normalized) return null;
    let body = normalized.body;
    const senderId = String((update.message ?? update.edited_message)?.from?.id ?? "");
    if (normalized.isPrivate && senderId && this.opts.db) {
      const status = pairingStatus(this.opts.db, senderId);
      if (status !== "approved") {
        const pending = ensurePendingPairing(this.opts.db, {
          senderId,
          senderName: normalized.sender,
          conversationExternalId: normalized.convExternalId,
        });
        body = unpairedBody(body, pending);
      }
    }
    (this.opts.emit ?? emitEvent)({
      type: "message",
      convExternalId: normalized.convExternalId,
      convTitle: normalized.convTitle,
      direction: "IN",
      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; chatId: string; text: string; threadId?: string | number; editMessageId?: string | number }) {
    if (!this.opts.transport) throw new Error("telegram transport is required");
    const send = async () => {
      if (cmd.editMessageId && this.opts.transport?.editMessageText) {
        return withTelegramRateLimit(
          () => this.opts.transport!.editMessageText!(cmd.chatId, cmd.editMessageId!, cmd.text, threadOptions(cmd.threadId)),
          { sleepMs: this.opts.sleepMs },
        );
      }
      return withTelegramRateLimit(
        () => this.opts.transport!.sendMessage(cmd.chatId, cmd.text, threadOptions(cmd.threadId)),
        { sleepMs: this.opts.sleepMs },
      );
    };
    return sendWithAck(
      { draftId: cmd.draftId, chatId: cmd.chatId, text: cmd.text },
      send,
      (result) => telegramProviderMsgId(result, String(cmd.chatId)),
      this.opts.emit ?? emitEvent,
    );
  }

  async react(chatId: string, messageId: string | number, emoji: string) {
    if (!this.opts.transport?.setMessageReaction) throw new Error("Telegram reactions are unavailable");
    return this.opts.transport.setMessageReaction(chatId, messageId, emoji);
  }
}

function threadOptions(threadId?: string | number): Record<string, unknown> | undefined {
  return threadId == null || String(threadId).trim() === "" ? undefined : { message_thread_id: Number(threadId) };
}

export function telegramStatusFromDb(db: DatabaseSync, env: NodeJS.ProcessEnv = process.env): TelegramAdapterStatus {
  ensureTelegramSchema(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='telegram_bot_token' LIMIT 1`).get() as
    | { value?: string | null }
    | undefined;
  const token = String(row?.value ?? env.TELEGRAM_BOT_TOKEN ?? "").trim();
  return {
    configured: !!token,
    inert: !token,
    connected: false,
    username: null,
    account_id: env.TELEGRAM_ACCOUNT_ID || "default",
    pending_pairings: pairingCounts(db, PAIRING).pending,
    approved_senders: pairingCounts(db, PAIRING).approved,
    last_error: null,
  };
}

export function conversationIdForTelegram(externalId: string, workspaceId = "local"): string {
  return `web:${workspaceId}:TELEGRAM:${externalId}`;
}

export function workspaceIdForDb(db: DatabaseSync): string {
  return selectedWorkspaceId(db);
}
