/**
 * Vaultfire Forum / XMTP V3 MLS Wrapper
 * ─────────────────────────────────────
 * Wires the Discussion Board to real XMTP V3 group messaging.
 *
 *  • One XMTP MLS group per forum category (general, technical, governance, tasks, announcements)
 *  • Group ID for each category is stored on-chain in VaultfireForumRegistry
 *      (contract deployed on Base, Avalanche, Arbitrum, Polygon — see app/lib/contracts.ts)
 *  • Posts/replies/upvotes are sent inside the group as JSON envelopes via group.sendText()
 *  • First user to post in a category creates the group and registers it on-chain (first-write-wins)
 *
 * SECURITY:
 *  • No private keys ever touch this module — signing happens via window.ethereum.personal_sign
 *  • Registry writes go through a wallet transaction on the user's connected chain
 */
import { Client, IdentifierKind, type Signer, type Identifier, type ClientOptions } from '@xmtp/browser-sdk';
import { FORUM_REGISTRY, CHAINS, type SupportedChain } from './contracts';

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

export type ForumCategory = 'general' | 'technical' | 'governance' | 'tasks' | 'announcements';

export const FORUM_CATEGORIES: ForumCategory[] = [
  'general',
  'technical',
  'governance',
  'tasks',
  'announcements',
];

/** Envelope shape posted as the body of every XMTP group message. */
export type ForumEnvelope =
  | { kind: 'post'; postId: string; title: string; content: string; category: ForumCategory; author: string; authorAddress: string; chain: SupportedChain; timestamp: number }
  | { kind: 'reply'; postId: string; replyId: string; content: string; author: string; authorAddress: string; timestamp: number }
  | { kind: 'upvote'; postId: string; voter: string; timestamp: number }
  | { kind: 'unvote'; postId: string; voter: string; timestamp: number };

export type XmtpStatus = 'idle' | 'connecting' | 'connected' | 'fallback' | 'error';

export interface XmtpEnv {
  status: XmtpStatus;
  address: string | null;
  inboxId: string | null;
  error: string | null;
}

// ─────────────────────────────────────────────────────────────────────────────
// Window wallet typing (mobile Base App / wagmi inject the same shape)
// ─────────────────────────────────────────────────────────────────────────────
interface EthRequestArgs { method: string; params?: unknown[] }
interface InjectedEth { request: (args: EthRequestArgs) => Promise<unknown> }
declare global {
  interface Window { ethereum?: InjectedEth }
}

// ─────────────────────────────────────────────────────────────────────────────
// XMTP signer factory — wraps window.ethereum into the V3 EOASigner shape
// ─────────────────────────────────────────────────────────────────────────────
function makeSigner(address: string): Signer {
  const lower = address.toLowerCase();
  return {
    type: 'EOA',
    getIdentifier: (): Identifier => ({
      identifier: lower,
      identifierKind: IdentifierKind.Ethereum,
    }),
    signMessage: async (message: string): Promise<Uint8Array> => {
      if (typeof window === 'undefined' || !window.ethereum) {
        throw new Error('No injected wallet found');
      }
      const sigHex = (await window.ethereum.request({
        method: 'personal_sign',
        params: [message, address],
      })) as string;
      // strip 0x and convert hex to Uint8Array
      const clean = sigHex.startsWith('0x') ? sigHex.slice(2) : sigHex;
      const bytes = new Uint8Array(clean.length / 2);
      for (let i = 0; i < bytes.length; i++) {
        bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
      }
      return bytes;
    },
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Registry RPC — read & write groupIds on VaultfireForumRegistry
// ─────────────────────────────────────────────────────────────────────────────

/** Function selectors (verified against compiled artifact). */
const SELECTOR_GET_GROUP = '0xabef281e';        // getGroup(string)
const SELECTOR_REGISTER_GROUP = '0x98f9cdb3';   // registerGroup(string,bytes)

function rpcUrl(chain: SupportedChain): string {
  return CHAINS[chain].rpcUrls.default.http[0];
}

async function jsonRpc(url: string, method: string, params: unknown[]): Promise<unknown> {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), 15000);
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
      signal: ctrl.signal,
    });
    const json = (await res.json()) as { result?: unknown; error?: { message: string } };
    if (json.error) throw new Error(json.error.message);
    return json.result;
  } finally {
    clearTimeout(t);
  }
}

/** Encodes a single dynamic string argument for an ABI call. */
function encodeStringArg(s: string): string {
  // Offset of dynamic data = 0x20 (32 bytes)
  const offset = '0000000000000000000000000000000000000000000000000000000000000020';
  const bytes = new TextEncoder().encode(s);
  const len = bytes.length.toString(16).padStart(64, '0');
  // Pad data to 32-byte boundary
  let hex = '';
  for (const b of bytes) hex += b.toString(16).padStart(2, '0');
  const padded = (hex + '0'.repeat(64)).slice(0, Math.ceil(hex.length / 64) * 64);
  return offset + len + padded;
}

/** Encodes (string, bytes) for registerGroup. */
function encodeStringBytesArgs(s: string, bytes: Uint8Array): string {
  // Two dynamic params: offsets at 0x40 and (0x40 + 0x20 + ceil(strBytes/32)*32)
  const strBytes = new TextEncoder().encode(s);
  const strLen = strBytes.length;
  const strChunks = Math.ceil(strLen / 32);
  const off1 = (0x40).toString(16).padStart(64, '0');
  const off2 = (0x40 + 0x20 + strChunks * 32).toString(16).padStart(64, '0');
  // string section
  const strLenHex = strLen.toString(16).padStart(64, '0');
  let strData = '';
  for (const b of strBytes) strData += b.toString(16).padStart(2, '0');
  strData = (strData + '0'.repeat(64)).slice(0, strChunks * 64);
  // bytes section
  const bLen = bytes.length;
  const bChunks = Math.ceil(bLen / 32);
  const bLenHex = bLen.toString(16).padStart(64, '0');
  let bData = '';
  for (const b of bytes) bData += b.toString(16).padStart(2, '0');
  bData = (bData + '0'.repeat(64)).slice(0, bChunks * 64);
  return off1 + off2 + strLenHex + strData + bLenHex + bData;
}

/**
 * Returns the registered groupId for a category, or null if unregistered.
 * groupId is XMTP V3 group id encoded as hex string.
 */
export async function readGroupId(chain: SupportedChain, category: ForumCategory): Promise<string | null> {
  const data = SELECTOR_GET_GROUP + encodeStringArg(category);
  const result = (await jsonRpc(rpcUrl(chain), 'eth_call', [
    { to: FORUM_REGISTRY[chain], data },
    'latest',
  ])) as string;
  if (!result || result === '0x') return null;
  // Decode (bytes groupId, address publisher, uint256 publishedAt, uint256 updatedAt)
  // Layout: head=[offsetToBytes (32), publisher (32), publishedAt (32), updatedAt (32)], then bytes data
  const hex = result.slice(2);
  if (hex.length < 64 * 4) return null;
  const bytesOffset = parseInt(hex.slice(0, 64), 16) * 2;
  if (bytesOffset + 64 > hex.length) return null;
  const bytesLen = parseInt(hex.slice(bytesOffset, bytesOffset + 64), 16);
  if (bytesLen === 0) return null;
  const bytesData = hex.slice(bytesOffset + 64, bytesOffset + 64 + bytesLen * 2);
  return '0x' + bytesData;
}

/** Reads which categories are already registered, parallelized across one chain. */
export async function readAllRegisteredGroups(
  chain: SupportedChain,
): Promise<Record<ForumCategory, string | null>> {
  const entries = await Promise.all(
    FORUM_CATEGORIES.map(async (c) => [c, await readGroupId(chain, c).catch(() => null)] as const),
  );
  const out: Record<string, string | null> = {};
  for (const [c, g] of entries) out[c] = g;
  return out as Record<ForumCategory, string | null>;
}

/**
 * Sends a registerGroup() transaction via the user's wallet.
 * Will revert on-chain if the category was claimed by someone else first (first-write-wins).
 *
 * Returns the tx hash.
 */
export async function registerGroupOnChain(
  chain: SupportedChain,
  category: ForumCategory,
  groupIdHex: string,
  fromAddress: string,
): Promise<string> {
  if (typeof window === 'undefined' || !window.ethereum) {
    throw new Error('No injected wallet');
  }
  // groupIdHex may be raw hex or 0x-prefixed
  const clean = groupIdHex.startsWith('0x') ? groupIdHex.slice(2) : groupIdHex;
  const bytes = new Uint8Array(clean.length / 2);
  for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
  const data = SELECTOR_REGISTER_GROUP + encodeStringBytesArgs(category, bytes);
  const tx = (await window.ethereum.request({
    method: 'eth_sendTransaction',
    params: [{
      from: fromAddress,
      to: FORUM_REGISTRY[chain],
      data,
    }],
  })) as string;
  return tx;
}

// ─────────────────────────────────────────────────────────────────────────────
// XMTP Client lifecycle (one client per address)
// ─────────────────────────────────────────────────────────────────────────────

let xmtpClient: Awaited<ReturnType<typeof Client.create>> | null = null;
let xmtpAddress: string | null = null;

export async function getOrCreateXmtpClient(address: string): Promise<Awaited<ReturnType<typeof Client.create>>> {
  if (xmtpClient && xmtpAddress?.toLowerCase() === address.toLowerCase()) return xmtpClient;
  const signer = makeSigner(address);
  // Production XMTP V3 network. ClientOptions is a discriminated union
  // (NetworkOptions | { backend }) so we type the literal explicitly.
  const opts: ClientOptions = { env: 'production' };
  xmtpClient = await Client.create(signer, opts);
  xmtpAddress = address.toLowerCase();
  return xmtpClient;
}

export function getXmtpClient(): Awaited<ReturnType<typeof Client.create>> | null {
  return xmtpClient;
}

export function clearXmtpClient(): void {
  xmtpClient = null;
  xmtpAddress = null;
}

// ─────────────────────────────────────────────────────────────────────────────
// Group helpers
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Resolves the local Group object for a category on the given chain.
 *  - If on-chain registration exists, look up the group by id.
 *  - Otherwise return null. Caller should prompt user to "claim" the category.
 */
export async function getGroupForCategory(
  chain: SupportedChain,
  category: ForumCategory,
): Promise<unknown | null> {
  if (!xmtpClient) return null;
  const groupId = await readGroupId(chain, category);
  if (!groupId) return null;
  const conv = await xmtpClient.conversations.getConversationById(groupId);
  return conv ?? null;
}

/**
 * Creates a new MLS group for `category` and registers it on-chain.
 * The creator is the only initial member; XMTP V3 groups are open-add by default
 * (PermissionsOptions.Default), so other agents can be added later by any member.
 *
 * Reverts if the category was already registered (first-write-wins).
 */
export async function createAndRegisterCategoryGroup(
  chain: SupportedChain,
  category: ForumCategory,
  fromAddress: string,
): Promise<{ groupId: string; txHash: string }> {
  if (!xmtpClient) throw new Error('XMTP client not initialised');
  // Pre-flight: someone may have registered this category between page load and now
  const existing = await readGroupId(chain, category);
  if (existing) throw new Error(`Category "${category}" is already registered`);

  // Create group with just the creator (others auto-discoverable via getConversationById once they join)
  const group = await xmtpClient.conversations.createGroup([], {
    groupName: `Vaultfire Forum · ${category}`,
    groupDescription: `Public XMTP V3 MLS group for the "${category}" forum category. Posts encoded as JSON envelopes.`,
  });
  const groupId = group.id;
  const txHash = await registerGroupOnChain(chain, category, groupId, fromAddress);
  return { groupId, txHash };
}

/** Sends a JSON envelope inside an XMTP group. */
export async function sendEnvelope(
  group: { sendText: (text: string) => Promise<string> },
  envelope: ForumEnvelope,
): Promise<string> {
  return group.sendText(JSON.stringify(envelope));
}

/** Parses an envelope from a message body. Returns null on non-envelope text. */
export function parseEnvelope(body: string): ForumEnvelope | null {
  try {
    const parsed = JSON.parse(body) as { kind?: string };
    if (
      parsed && typeof parsed === 'object' &&
      typeof parsed.kind === 'string' &&
      ['post', 'reply', 'upvote', 'unvote'].includes(parsed.kind)
    ) {
      return parsed as ForumEnvelope;
    }
    return null;
  } catch {
    return null;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// localStorage draft preservation
// ─────────────────────────────────────────────────────────────────────────────

const LEGACY_STORAGE_KEY = 'vaultfire_hub_discussion_v1';
const DRAFT_MIGRATED_KEY = 'vaultfire_hub_discussion_drafts_marked_v1';

/**
 * Migrates legacy localStorage discussion posts to the "local draft" pool.
 * Idempotent — runs only once per browser. Returns the marked drafts.
 */
export interface LocalDraftPost {
  id: string;
  title: string;
  content: string;
  category: ForumCategory | string;
  author: string;
  authorAddress: string;
  timestamp: number;
  upvotes: number;
  upvoters: string[];
  replies: Array<{ id: string; author: string; authorAddress: string; content: string; timestamp: number }>;
  isLocalDraft: true;
}

export function readLocalDrafts(): LocalDraftPost[] {
  if (typeof window === 'undefined') return [];
  try {
    const raw = window.localStorage.getItem(LEGACY_STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw) as Array<Record<string, unknown>>;
    if (!Array.isArray(parsed)) return [];
    return parsed.map((p) => ({
      id: String(p.id ?? ''),
      title: String(p.title ?? ''),
      content: String(p.content ?? ''),
      category: String(p.category ?? 'general') as ForumCategory,
      author: String(p.author ?? 'anon'),
      authorAddress: String(p.authorAddress ?? '0x0'),
      timestamp: Number(p.timestamp ?? Date.now()),
      upvotes: Number(p.upvotes ?? 0),
      upvoters: Array.isArray(p.upvoters) ? (p.upvoters as string[]) : [],
      replies: Array.isArray(p.replies) ? (p.replies as LocalDraftPost['replies']) : [],
      isLocalDraft: true,
    }));
  } catch {
    return [];
  }
}

export function markDraftsAsMigrated(): void {
  if (typeof window === 'undefined') return;
  try {
    window.localStorage.setItem(DRAFT_MIGRATED_KEY, '1');
  } catch { /* ignore */ }
}

export function hasMarkedDrafts(): boolean {
  if (typeof window === 'undefined') return false;
  try {
    return window.localStorage.getItem(DRAFT_MIGRATED_KEY) === '1';
  } catch {
    return false;
  }
}
