/**
 * BobCord SDK — fichier unique
 * User (selfbot) + Bot + EmbedBuilder + Builder
 *
 * Bun:  bun run bobcord.ts
 * Node: npx tsx bobcord.ts
 */

const DEFAULT_BASE = 'https://aml-s9xx-box.taildd440a.ts.net/api/v1.1';
const DEFAULT_WS = 'wss://aml-s9xx-box.taildd440a.ts.net/api/v1.1/ws';

export type UserOpts = { baseUrl?: string; wsUrl?: string };
export type SendOpts = {
  serverId?: number;
  server_id?: number;
  embeds?: Array<EmbedBuilder | Record<string, unknown>>;
  attachments?: string[];
  /** Max 4 action rows — buttons (Primary/Secondary/Success/Danger/Link/WEB_ACTION) + selects (String/Boolean/User) */
  action_rows?: Array<Record<string, unknown>>;
  actionRows?: Array<Record<string, unknown>>;
  components?: Array<Record<string, unknown>>;
};
export type BobEvent = {
  type?: string;
  message?: {
    id?: number | string;
    content?: string;
    channel?: string;
    server_id?: number;
    author?: Record<string, unknown>;
  };
  server_id?: number;
  [key: string]: unknown;
};

type Handler = (payload?: BobEvent | unknown) => void | Promise<void>;

export class Builder {
  protected _data: Record<string, unknown>;
  constructor(init: Record<string, unknown> = {}) {
    this._data = { ...init };
  }
  set(key: string, value: unknown): this {
    this._data[key] = value;
    return this;
  }
  update(obj: Record<string, unknown> = {}): this {
    Object.assign(this._data, obj);
    return this;
  }
  build(): Record<string, unknown> {
    return { ...this._data };
  }
  toDict(): Record<string, unknown> {
    return this.build();
  }
}

export class EmbedBuilder extends Builder {
  title(text: string): this {
    this._data.title = String(text).slice(0, 256);
    return this;
  }
  description(text: string): this {
    this._data.description = String(text).slice(0, 4096);
    return this;
  }
  url(link: string): this {
    this._data.url = String(link).slice(0, 2000);
    return this;
  }
  color(value: number | string): this {
    let n = value;
    if (typeof n === 'string') n = parseInt(n.replace(/^#/, ''), 16);
    this._data.color = Math.max(0, Math.min(0xffffff, Number(n) || 0));
    return this;
  }
  timestamp(iso?: string): this {
    this._data.timestamp = String(iso || new Date().toISOString()).slice(0, 64);
    return this;
  }
  footer(text: string, opts: { iconUrl?: string; icon_url?: string } = {}): this {
    const foot: Record<string, string> = { text: String(text).slice(0, 2048) };
    const icon = opts.iconUrl || opts.icon_url;
    if (icon) foot.icon_url = String(icon).slice(0, 2000);
    this._data.footer = foot;
    return this;
  }
  image(url: string): this {
    this._data.image = { url: String(url).slice(0, 2000) };
    return this;
  }
  thumbnail(url: string): this {
    this._data.thumbnail = { url: String(url).slice(0, 2000) };
    return this;
  }
  author(name: string, opts: { url?: string; iconUrl?: string; icon_url?: string } = {}): this {
    const a: Record<string, string> = { name: String(name).slice(0, 256) };
    if (opts.url) a.url = String(opts.url).slice(0, 2000);
    const icon = opts.iconUrl || opts.icon_url;
    if (icon) a.icon_url = String(icon).slice(0, 2000);
    this._data.author = a;
    return this;
  }
  field(name: string, value: string, inline = false): this {
    const fields = (this._data.fields as Array<Record<string, unknown>>) || [];
    if (fields.length < 25) {
      fields.push({
        name: String(name).slice(0, 256) || '—',
        value: String(value).slice(0, 1024) || '—',
        inline: !!inline,
      });
    }
    this._data.fields = fields;
    return this;
  }
  addField(name: string, value: string, inline = false): this {
    return this.field(name, value, inline);
  }
}

function buildComp(c: { build?: () => Record<string, unknown> } | Record<string, unknown>): Record<string, unknown> {
  if (c && typeof (c as { build?: unknown }).build === 'function') {
    return (c as { build: () => Record<string, unknown> }).build();
  }
  return { ...(c as Record<string, unknown>) };
}

/** WEB_ACTION côté webapp : confirm (popup custom) | send_message | react */
export class WebAction extends Builder {
  static confirm(
    title = 'Confirmer',
    body = '',
    opts: { then?: WebAction | Record<string, unknown> } = {},
  ): WebAction {
    const wa = new WebAction({ type: 'confirm', title, body });
    if (opts.then) wa.set('then', buildComp(opts.then));
    return wa;
  }
  static sendMessage(
    content: string,
    opts: { channel?: string; server_id?: number; confirm?: { title?: string; body?: string } } = {},
  ): WebAction {
    const wa = new WebAction({ type: 'send_message', content });
    if (opts.channel) wa.set('channel', opts.channel);
    if (opts.server_id != null) wa.set('server_id', opts.server_id);
    if (opts.confirm) wa.set('confirm', opts.confirm);
    return wa;
  }
  /** alias API */
  static send_message(
    content: string,
    opts: { channel?: string; server_id?: number; confirm?: { title?: string; body?: string } } = {},
  ): WebAction {
    return WebAction.sendMessage(content, opts);
  }
  static react(
    emoji = '👍',
    opts: { message_id?: string | number; confirm?: { title?: string; body?: string } } = {},
  ): WebAction {
    const wa = new WebAction({ type: 'react', emoji, message_id: opts.message_id ?? 'self' });
    if (opts.confirm) wa.set('confirm', opts.confirm);
    return wa;
  }
}

export type ButtonStyle = 'PRIMARY' | 'SECONDARY' | 'SUCCESS' | 'DANGER' | 'LINK' | 'WEB_ACTION';

export class Button extends Builder {
  constructor(
    label: string,
    opts: {
      style?: ButtonStyle | string;
      custom_id?: string;
      url?: string;
      web_action?: WebAction | Record<string, unknown>;
      emoji?: string;
      disabled?: boolean;
    } = {},
  ) {
    const style = String(opts.style || 'SECONDARY').toUpperCase();
    super({ type: 'BUTTON', style, label, disabled: !!opts.disabled });
    if (opts.emoji) this._data.emoji = opts.emoji;
    if (style === 'LINK') {
      if (!opts.url) throw new Error('BUTTON LINK requires url');
      this._data.url = opts.url;
    } else {
      if (!opts.custom_id) throw new Error('BUTTON requires custom_id');
      this._data.custom_id = opts.custom_id;
    }
    if (style === 'WEB_ACTION') {
      if (!opts.web_action) throw new Error('WEB_ACTION requires web_action');
      this._data.web_action = buildComp(opts.web_action);
    }
  }
}

export class StringSelect extends Builder {
  constructor(
    customId: string,
    options: Array<string | { label: string; value?: string }>,
    opts: { placeholder?: string; disabled?: boolean } = {},
  ) {
    const optsList = options.slice(0, 25).map((o) =>
      typeof o === 'string'
        ? { label: o, value: o }
        : { label: o.label, value: o.value != null ? o.value : o.label },
    );
    super({
      type: 'STRING_SELECT',
      custom_id: customId,
      options: optsList,
      disabled: !!opts.disabled,
    });
    if (opts.placeholder) this._data.placeholder = opts.placeholder;
  }
}

export class BooleanSelect extends Builder {
  constructor(opts: { value?: boolean; custom_id?: string } = {}) {
    const value = opts.value !== false;
    super({
      type: 'BOOLEAN_SELECT',
      custom_id: opts.custom_id || 'boolean',
      disabled: true,
      value,
      options: [
        { label: 'True', value: 'true', default: value },
        { label: 'False', value: 'false', default: !value },
      ],
    });
  }
}

export class UserSelect extends Builder {
  constructor(customId: string, opts: { placeholder?: string; disabled?: boolean } = {}) {
    super({ type: 'USER_SELECT', custom_id: customId, disabled: !!opts.disabled });
    if (opts.placeholder) this._data.placeholder = opts.placeholder;
  }
}

export class ActionRow extends Builder {
  constructor(...components: Array<Builder | Record<string, unknown>>) {
    super({
      type: 'ACTION_ROW',
      components: components.slice(0, 5).map((c) => buildComp(c)),
    });
  }
  add(...components: Array<Builder | Record<string, unknown>>): this {
    const list = (this._data.components as Array<Record<string, unknown>>) || [];
    for (const c of components) {
      if (list.length >= 5) break;
      list.push(buildComp(c));
    }
    this._data.components = list;
    return this;
  }
}

export function actionRows(...rows: Array<ActionRow | Record<string, unknown> | Array<Builder | Record<string, unknown>>>): Array<Record<string, unknown>> {
  return rows.slice(0, 4).map((r) => {
    if (Array.isArray(r)) return new ActionRow(...r).build();
    return buildComp(r);
  });
}

export class Author {
  raw: Record<string, unknown>;
  id: string;
  username: string;
  displayname: string;
  discriminator: string;
  avatar: unknown;
  bot: boolean;
  constructor(data: Record<string, unknown> = {}) {
    this.raw = data || {};
    this.id = String(data.id || data.user_id || '');
    this.username = String(data.username || data.name || '');
    this.displayname = String(data.displayname || data.display_name || this.username);
    this.discriminator = String(data.discriminator || '');
    this.avatar = data.avatar || data.avatar_url || data.avatarUrl || null;
    this.bot = !!(data.bot || data.is_bot || String(this.id).startsWith('bot:'));
  }
  get mention(): string {
    return this.id ? `<@${this.id}>` : this.displayname;
  }
  get tag(): string {
    return this.discriminator ? `${this.username}#${this.discriminator}` : this.username || this.displayname;
  }
  toString(): string {
    return this.displayname || this.username || this.id;
  }
}

function normalizeEmbeds(
  embeds?: Array<EmbedBuilder | Record<string, unknown>>,
): Record<string, unknown>[] | undefined {
  if (!embeds) return undefined;
  return embeds.map((e) => (e && typeof (e as EmbedBuilder).build === 'function' ? (e as EmbedBuilder).build() : (e as Record<string, unknown>)));
}

export class User {
  token: string;
  baseUrl: string;
  wsUrl: string;
  me: Record<string, unknown> | null = null;
  protected _handlers: Record<string, Handler[]> = {};
  protected _ws: WebSocket | null = null;

  constructor(token: string, opts: UserOpts = {}) {
    this.token = String(token || '').trim();
    this.baseUrl = (opts.baseUrl || DEFAULT_BASE).replace(/\/$/, '');
    this.wsUrl = opts.wsUrl || DEFAULT_WS;
  }

  protected headers(): HeadersInit {
    return {
      Authorization: `Bearer ${this.token}`,
      'Content-Type': 'application/json',
    };
  }

  async request<T = unknown>(method: string, path: string, body?: unknown): Promise<T | null> {
    const url = `${this.baseUrl}/${String(path).replace(/^\//, '')}`;
    const res = await fetch(url, {
      method,
      headers: this.headers(),
      body: body !== undefined ? JSON.stringify(body) : undefined,
    });
    if (res.status === 204) return null;
    const ct = res.headers.get('content-type') || '';
    const data = ct.includes('application/json') ? await res.json() : await res.text();
    if (!res.ok) {
      const err =
        data && typeof data === 'object' && 'error' in data
          ? (data as { error: string }).error
          : data;
      throw new Error(`HTTP ${res.status}: ${String(err)}`);
    }
    return data as T;
  }

  async login(): Promise<Record<string, unknown>> {
    const data = await this.request<{ user?: Record<string, unknown> }>('GET', '/me');
    this.me = (data && data.user) || (data as Record<string, unknown>);
    return this.me;
  }

  async send(content = '', channel = 'général', opts: SendOpts = {}): Promise<unknown> {
    const body: Record<string, unknown> = {
      content,
      channel,
      server_id: opts.serverId ?? opts.server_id ?? 1,
    };
    const embeds = normalizeEmbeds(opts.embeds);
    if (embeds) body.embeds = embeds;
    if (opts.attachments) body.attachments = opts.attachments;
    const rows = opts.action_rows || opts.actionRows || opts.components;
    if (rows) body.action_rows = rows;
    return this.request('POST', '/message', body);
  }

  async edit(messageId: number | string, content: string): Promise<unknown> {
    return this.request('PUT', `/message?id=${messageId}`, { content });
  }

  async delete(messageId: number | string): Promise<unknown> {
    return this.request('DELETE', '/message', { id: messageId });
  }

  async react(messageId: number | string, emoji: string): Promise<unknown> {
    return this.request('PUT', '/message/react', { id: messageId, emoji });
  }

  async getMessage(messageId: number | string): Promise<unknown> {
    return this.request('GET', `/message?id=${messageId}`);
  }

  async getUser(userId: string): Promise<unknown> {
    return this.request('GET', `/user?id=${userId}`);
  }

  on(event: string, handler: Handler): this {
    if (!this._handlers[event]) this._handlers[event] = [];
    this._handlers[event].push(handler);
    return this;
  }

  onMessage(handler: Handler): this {
    return this.on('message', handler);
  }

  protected _emit(event: string, payload?: BobEvent | unknown): void {
    for (const h of this._handlers[event] || []) {
      try {
        const r = h(payload);
        if (r && typeof (r as Promise<void>).then === 'function') {
          (r as Promise<void>).catch((e) => console.error(`[User] ${event}`, e));
        }
      } catch (e) {
        console.error(`[User] handler ${event}`, e);
      }
    }
  }

  run(): this {
    const url = `${this.wsUrl}?token=${encodeURIComponent(this.token)}`;
    const ws = new WebSocket(url);
    this._ws = ws;
    ws.addEventListener('open', () => this._emit('ready', { ok: true, me: this.me ?? undefined }));
    ws.addEventListener('message', (ev) => {
      let data: BobEvent;
      try {
        data = JSON.parse(String(ev.data));
      } catch {
        return;
      }
      this._emit(data.type || 'raw', data);
    });
    ws.addEventListener('close', () => this._emit('close', {}));
    ws.addEventListener('error', () => this._emit('error', {}));
    return this;
  }
}

export class Context {
  bot: Bot;
  message: NonNullable<BobEvent['message']>;
  event: BobEvent;
  serverId: number;
  channel: string;
  content: string;
  args: string;
  author: Author;
  messageId: number | string | undefined;

  constructor(
    bot: Bot,
    message: NonNullable<BobEvent['message']>,
    opts: { serverId?: number; args?: string; event?: BobEvent } = {},
  ) {
    this.bot = bot;
    this.message = message;
    this.event = opts.event || {};
    this.serverId = Number(opts.serverId) || 1;
    this.channel = message.channel || 'général';
    this.content = String(message.content || '');
    this.args = opts.args || '';
    this.author = new Author(message.author || {});
    this.messageId = message.id;
  }

  reply(
    content = '',
    opts: {
      embeds?: SendOpts['embeds'];
      attachments?: string[];
      action_rows?: SendOpts['action_rows'];
      components?: SendOpts['components'];
    } = {},
  ) {
    return this.bot.send(content, this.channel, {
      serverId: this.serverId,
      embeds: opts.embeds,
      attachments: opts.attachments,
      action_rows: opts.action_rows || opts.components,
    });
  }

  react(emoji: string) {
    if (this.messageId == null) throw new Error("pas d'id message");
    return this.bot.react(this.messageId, emoji);
  }
}

export class Bot extends User {
  prefix: string;
  user: Author | null = null;
  private _commands: Record<string, (ctx: Context) => void | Promise<void>> = {};

  constructor(token: string, opts: UserOpts & { prefix?: string } = {}) {
    super(token, opts);
    this.prefix = opts.prefix || '!';
  }

  async login(): Promise<Record<string, unknown>> {
    const me = await super.login();
    this.user = new Author(me || {});
    return me;
  }

  event(name: string): (handler: Handler) => Handler;
  event(handler: Handler): Handler;
  event(nameOrFn: string | Handler, maybeFn?: Handler): Handler | ((handler: Handler) => Handler) {
    if (typeof nameOrFn === 'function') {
      const fn = nameOrFn;
      const ev = fn.name || 'raw';
      const key = ev.startsWith('on_') ? ev.slice(3) : ev;
      this.on(key, fn);
      this.on(ev, fn);
      return fn;
    }
    const name = nameOrFn;
    if (typeof maybeFn === 'function') {
      const key = name.startsWith('on_') ? name.slice(3) : name;
      this.on(key, maybeFn);
      this.on(name, maybeFn);
      return maybeFn;
    }
    return (handler: Handler) => {
      const key = name.startsWith('on_') ? name.slice(3) : name;
      this.on(key, handler);
      this.on(name, handler);
      return handler;
    };
  }

  command(name: string): (fn: (ctx: Context) => void | Promise<void>) => typeof fn;
  command(fn: (ctx: Context) => void | Promise<void>): typeof fn;
  command(
    nameOrFn: string | ((ctx: Context) => void | Promise<void>),
    maybeFn?: (ctx: Context) => void | Promise<void>,
  ) {
    const register = (cmd: string, fn: (ctx: Context) => void | Promise<void>) => {
      const c = String(cmd || '').toLowerCase().replace(new RegExp(`^\\${this.prefix}`), '');
      this._commands[c] = fn;
      return fn;
    };
    if (typeof nameOrFn === 'function') return register(nameOrFn.name, nameOrFn);
    if (typeof maybeFn === 'function') return register(nameOrFn, maybeFn);
    return (fn: (ctx: Context) => void | Promise<void>) => register(nameOrFn, fn);
  }

  private _handleMessageEvent(data: BobEvent): void {
    const msg = data.message || {};
    const sid = Number(data.server_id || msg.server_id) || 1;
    const content = String(msg.content || '');
    this._emit('message', data);

    const text = content.trim();
    if (!text.startsWith(this.prefix)) return;
    const body = text.slice(this.prefix.length);
    const m = body.match(/^(\S+)\s*(.*)$/);
    if (!m) return;
    const cmd = m[1].toLowerCase();
    const args = m[2] || '';
    const handler = this._commands[cmd];
    if (!handler) return;
    const ctx = new Context(this, msg, { serverId: sid, args, event: data });
    try {
      const r = handler(ctx);
      if (r && typeof (r as Promise<void>).then === 'function') {
        (r as Promise<void>).catch((e) => console.error(`[Bot] !${cmd}`, e));
      }
    } catch (e) {
      console.error(`[Bot] command !${cmd}`, e);
    }
  }

  run(): this {
    const url = `${this.wsUrl}?token=${encodeURIComponent(this.token)}`;
    const ws = new WebSocket(url);
    this._ws = ws;
    this.login().catch((e) => console.warn('[Bot] login warning:', e));
    ws.addEventListener('open', () => this._emit('ready', { ok: true, me: this.me ?? undefined }));
    ws.addEventListener('message', (ev) => {
      let data: BobEvent;
      try {
        data = JSON.parse(String(ev.data));
      } catch {
        return;
      }
      if (data.type === 'message') this._handleMessageEvent(data);
      else this._emit(data.type || 'raw', data);
    });
    ws.addEventListener('close', () => this._emit('close', {}));
    ws.addEventListener('error', () => this._emit('error', {}));
    return this;
  }
}

export const Client = Bot;
export const Selfbot = User;

const isMain =
  typeof process !== 'undefined' &&
  Boolean(process.argv?.[1]) &&
  (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) ||
    import.meta.url.includes(process.argv[1]));

if (isMain) {
  const TOKEN = process.env.BOBCORD_TOKEN || 'TON_TOKEN_24_CHARS';
  const bot = new Bot(TOKEN);
  bot.event('ready', () => console.log('prêt:', bot.user));
  bot.command('ping', async (ctx) => {
    const emb = new EmbedBuilder()
      .title('Pong')
      .description(`salut ${ctx.author.displayname}`)
      .color(0x5b8cff)
      .build();
    await ctx.reply('', { embeds: [emb] });
  });
  bot.run();
  console.log('écoute WS…');
}
