import { Observable, firstValueFrom, combineLatest } from "rxjs";
import { map, first, toArray } from "rxjs/operators";
import type { Filter } from "nostr-tools";
import { nip19 } from "nostr-tools";
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import {
  ChatProtocolAdapter,
  type SendMessageOptions,
  type ZapConfig,
} from "./base-adapter";
import type {
  Conversation,
  Message,
  ProtocolIdentifier,
  ChatCapabilities,
  LoadMessagesOptions,
  Participant,
} from "@/types/chat";
import type { NostrEvent } from "@/types/nostr";
import eventStore from "@/services/event-store";
import pool from "@/services/relay-pool";
import { publishEventToRelays } from "@/services/hub";
import accountManager from "@/services/accounts";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { mergeRelaySets } from "applesauce-core/helpers";
import { getOutboxes } from "applesauce-core/helpers/mailboxes";
import { getEventPointerFromETag } from "applesauce-core/helpers/pointers";
import { EventFactory } from "applesauce-core/event-factory";
import {
  NoteReplyBlueprint,
  ReactionBlueprint,
} from "applesauce-common/blueprints";
import { getNip10References } from "applesauce-common/helpers";
import {
  getZapAmount,
  getZapSender,
  getZapRecipient,
} from "applesauce-common/helpers";

/**
 * NIP-10 Adapter - Threaded Notes as Chat
 *
 * Features:
 * - Turn any kind 1 note thread into a chat interface
 * - Root event displayed prominently at top
 * - All replies shown as chat messages
 * - Proper NIP-10 tag structure (root/reply markers)
 * - Smart relay selection (merges multiple sources)
 *
 * Thread ID format: nevent1... or note1...
 * Events use "e" tags with markers ("root", "reply")
 */
export class Nip10Adapter extends ChatProtocolAdapter {
  readonly protocol = "nip-10" as const;
  readonly type = "group" as const; // Threads are multi-participant like groups

  /**
   * Parse identifier - accepts nevent or note format
   * Examples:
   *   - nevent1qqsxyz... (with relay hints, author, kind)
   *   - note1abc... (simple event ID)
   */
  parseIdentifier(input: string): ProtocolIdentifier | null {
    // Try note format first (simpler)
    if (input.startsWith("note1")) {
      try {
        const decoded = nip19.decode(input);
        if (decoded.type === "note") {
          const eventId = decoded.data as string;
          return {
            type: "thread",
            value: { id: eventId },
            relays: [],
          };
        }
      } catch {
        return null;
      }
    }

    // Try nevent format (includes relay hints)
    if (input.startsWith("nevent1")) {
      try {
        const decoded = nip19.decode(input);
        if (decoded.type === "nevent") {
          const { id, relays, author, kind } = decoded.data;

          // If kind is specified and NOT kind 1, let other adapters handle
          if (kind !== undefined && kind !== 1) {
            return null;
          }

          return {
            type: "thread",
            value: { id, relays, author, kind },
            relays: relays || [],
          };
        }
      } catch {
        return null;
      }
    }

    return null;
  }

  /**
   * Resolve conversation from thread identifier
   */
  async resolveConversation(
    identifier: ProtocolIdentifier,
  ): Promise<Conversation> {
    if (identifier.type !== "thread") {
      throw new Error(
        `NIP-10 adapter cannot handle identifier type: ${identifier.type}`,
      );
    }

    const pointer = identifier.value;
    const relayHints = identifier.relays || [];

    // 1. Fetch the provided event
    const providedEvent = await this.fetchEvent(pointer.id, relayHints);
    if (!providedEvent) {
      throw new Error("Event not found");
    }

    if (providedEvent.kind !== 1) {
      throw new Error(`Expected kind 1 note, got kind ${providedEvent.kind}`);
    }

    // 2. Parse NIP-10 references to find root
    const refs = getNip10References(providedEvent);
    let rootEvent: NostrEvent;
    let rootId: string;

    if (refs.root?.e) {
      // This is a reply - fetch the root
      rootId = refs.root.e.id;

      const fetchedRoot = await this.fetchEvent(
        rootId,
        refs.root.e.relays || [],
      );
      if (!fetchedRoot) {
        throw new Error("Thread root not found");
      }
      rootEvent = fetchedRoot;
    } else {
      // No root reference - this IS the root
      rootEvent = providedEvent;
      rootId = providedEvent.id;
    }

    // 3. Determine conversation relays
    const conversationRelays = await this.getThreadRelays(
      rootEvent,
      providedEvent,
      relayHints,
    );

    // 4. Extract title from root content
    const title = this.extractTitle(rootEvent);

    // 5. Build participants list from root and provided event
    const participants = this.extractParticipants(rootEvent, providedEvent);

    // 6. Build conversation object
    return {
      id: `nip-10:${rootId}`,
      type: "group",
      protocol: "nip-10",
      title,
      participants,
      metadata: {
        rootEventId: rootId,
        providedEventId: providedEvent.id,
        description: rootEvent.content.slice(0, 200), // First 200 chars
        relays: conversationRelays,
      },
      unreadCount: 0,
    };
  }

  /**
   * Load messages for a thread
   */
  loadMessages(
    conversation: Conversation,
    options?: LoadMessagesOptions,
  ): Observable<Message[]> {
    const rootEventId = conversation.metadata?.rootEventId;
    const relays = conversation.metadata?.relays || [];

    if (!rootEventId) {
      throw new Error("Root event ID required");
    }

    // Build filter for all thread events:
    // - kind 1: replies to root
    // - kind 6: reposts (legacy)
    // - kind 7: reactions
    // - kind 16: generic reposts
    // - kind 9735: zap receipts
    const filters: Filter[] = [
      // Replies: kind 1 events with e-tag pointing to root
      {
        kinds: [1],
        "#e": [rootEventId],
        limit: options?.limit || 100,
      },
      // Reactions: kind 7 events with e-tag pointing to root or replies
      {
        kinds: [7],
        "#e": [rootEventId],
        limit: 200, // Reactions are small, fetch more
      },
      // Reposts: kind 6 and 16 events with e-tag pointing to root or replies
      {
        kinds: [6, 16],
        "#e": [rootEventId],
        limit: 100,
      },
      // Zaps: kind 9735 receipts with e-tag pointing to root or replies
      {
        kinds: [9735],
        "#e": [rootEventId],
        limit: 100,
      },
    ];

    if (options?.before) {
      filters[0].until = options.before;
    }
    if (options?.after) {
      filters[0].since = options.after;
    }

    // Clean up any existing subscription
    const conversationId = `nip-10:${rootEventId}`;
    this.cleanup(conversationId);

    // Start persistent subscription
    const subscription = pool
      .subscription(relays, filters, { eventStore })
      .subscribe({
        next: (_response) => {
          // EOSE or event - both handled by EventStore
        },
      });

    // Store subscription for cleanup
    this.subscriptions.set(conversationId, subscription);

    // Return observable from EventStore
    // Combine root event with replies
    const rootEvent$ = eventStore.event(rootEventId);
    const replies$ = eventStore.timeline({
      kinds: [1, 6, 7, 16, 9735],
      "#e": [rootEventId],
    });

    return combineLatest([rootEvent$, replies$]).pipe(
      map(([rootEvent, replyEvents]) => {
        const messages: Message[] = [];

        // Add root event as first message
        if (rootEvent) {
          const rootMessage = this.rootEventToMessage(
            rootEvent,
            conversationId,
            rootEventId,
          );
          if (rootMessage) {
            messages.push(rootMessage);
          }
        }

        // Convert replies to messages
        const replyMessages = replyEvents
          .map((event) =>
            this.eventToMessage(event, conversationId, rootEventId),
          )
          .filter((msg): msg is Message => msg !== null);

        messages.push(...replyMessages);

        // Sort by timestamp ascending (chronological order)
        return messages.sort((a, b) => a.timestamp - b.timestamp);
      }),
    );
  }

  /**
   * Load more historical messages (pagination)
   */
  async loadMoreMessages(
    conversation: Conversation,
    before: number,
  ): Promise<Message[]> {
    const rootEventId = conversation.metadata?.rootEventId;
    const relays = conversation.metadata?.relays || [];

    if (!rootEventId) {
      throw new Error("Root event ID required");
    }

    // Same filters as loadMessages but with until for pagination
    const filters: Filter[] = [
      {
        kinds: [1],
        "#e": [rootEventId],
        until: before,
        limit: 50,
      },
      {
        kinds: [7],
        "#e": [rootEventId],
        until: before,
        limit: 100,
      },
      {
        kinds: [6, 16],
        "#e": [rootEventId],
        until: before,
        limit: 50,
      },
      {
        kinds: [9735],
        "#e": [rootEventId],
        until: before,
        limit: 50,
      },
    ];

    // One-shot request to fetch older messages
    const events = await firstValueFrom(
      pool.request(relays, filters, { eventStore }).pipe(toArray()),
    );

    const conversationId = `nip-10:${rootEventId}`;

    // Convert events to messages
    const messages = events
      .map((event) => this.eventToMessage(event, conversationId, rootEventId))
      .filter((msg): msg is Message => msg !== null);

    // Reverse for ascending chronological order
    return messages.reverse();
  }

  /**
   * Send a message (reply) to the thread
   */
  async sendMessage(
    conversation: Conversation,
    content: string,
    options?: SendMessageOptions,
  ): Promise<void> {
    const activePubkey = accountManager.active$.value?.pubkey;
    const activeSigner = accountManager.active$.value?.signer;

    if (!activePubkey || !activeSigner) {
      throw new Error("No active account or signer");
    }

    const rootEventId = conversation.metadata?.rootEventId;
    const relays = conversation.metadata?.relays || [];

    if (!rootEventId) {
      throw new Error("Root event ID required");
    }

    // Determine parent: either replyTo or root
    const parentEventId = options?.replyTo || rootEventId;
    const parentEvent = await firstValueFrom(eventStore.event(parentEventId), {
      defaultValue: undefined,
    });

    if (!parentEvent) {
      throw new Error(
        `${parentEventId === rootEventId ? "Root" : "Parent"} event not found in store`,
      );
    }

    // Create event factory
    const factory = new EventFactory();
    factory.setSigner(activeSigner);

    // Use NoteReplyBlueprint - automatically handles NIP-10 tags and p-tag copying!
    const draft = await factory.create(
      NoteReplyBlueprint,
      parentEvent,
      content,
      {
        emojis: options?.emojiTags?.map((e) => ({
          shortcode: e.shortcode,
          url: e.url,
        })),
      },
    );

    // Add NIP-92 imeta tags for blob attachments (not yet handled by applesauce)
    if (options?.blobAttachments) {
      for (const blob of options.blobAttachments) {
        const imetaParts = [`url ${blob.url}`];
        if (blob.sha256) imetaParts.push(`x ${blob.sha256}`);
        if (blob.mimeType) imetaParts.push(`m ${blob.mimeType}`);
        if (blob.size) imetaParts.push(`size ${blob.size}`);
        draft.tags.push(["imeta", ...imetaParts]);
      }
    }

    // Sign the event
    const event = await factory.sign(draft);

    // Publish to conversation relays
    await publishEventToRelays(event, relays);
  }

  /**
   * Send a reaction (kind 7) to a message in the thread
   */
  async sendReaction(
    conversation: Conversation,
    messageId: string,
    emoji: string,
    customEmoji?: { shortcode: string; url: string },
  ): Promise<void> {
    const activePubkey = accountManager.active$.value?.pubkey;
    const activeSigner = accountManager.active$.value?.signer;

    if (!activePubkey || !activeSigner) {
      throw new Error("No active account or signer");
    }

    const relays = conversation.metadata?.relays || [];

    // Fetch the message being reacted to
    const messageEvent = await firstValueFrom(eventStore.event(messageId), {
      defaultValue: undefined,
    });

    if (!messageEvent) {
      throw new Error("Message event not found");
    }

    // Create event factory
    const factory = new EventFactory();
    factory.setSigner(activeSigner);

    // Use ReactionBlueprint - auto-handles e-tag, k-tag, p-tag, custom emoji
    const emojiArg = customEmoji
      ? { shortcode: customEmoji.shortcode, url: customEmoji.url }
      : emoji;

    const draft = await factory.create(
      ReactionBlueprint,
      messageEvent,
      emojiArg,
    );

    // Sign the event
    const event = await factory.sign(draft);

    // Publish to conversation relays
    await publishEventToRelays(event, relays);
  }

  /**
   * Get zap configuration for a message in a NIP-10 thread
   * Returns configuration for how zap requests should be constructed
   */
  getZapConfig(message: Message, conversation: Conversation): ZapConfig {
    // Get relays from conversation metadata
    const relays = conversation.metadata?.relays || [];

    // Build eventPointer for the message being zapped
    const eventPointer = {
      id: message.id,
      author: message.author,
      relays,
    };

    // Recipient is the message author
    return {
      supported: true,
      recipientPubkey: message.author,
      eventPointer,
      relays,
    };
  }

  /**
   * Load a replied-to message by pointer
   */
  async loadReplyMessage(
    conversation: Conversation,
    pointer: EventPointer | AddressPointer,
  ): Promise<NostrEvent | null> {
    // Extract event ID from pointer (EventPointer has 'id', AddressPointer doesn't)
    const eventId = "id" in pointer ? pointer.id : null;

    if (!eventId) {
      console.warn(
        "[NIP-10] AddressPointer not supported for loadReplyMessage",
      );
      return null;
    }

    // First check EventStore - might already be loaded
    const cachedEvent = await eventStore
      .event(eventId)
      .pipe(first())
      .toPromise();
    if (cachedEvent) {
      return cachedEvent;
    }

    // Build relay list: conversation relays + pointer relay hints (deduplicated and normalized)
    const conversationRelays = conversation.metadata?.relays || [];
    const relays = mergeRelaySets(conversationRelays, pointer.relays || []);

    if (relays.length === 0) {
      console.warn("[NIP-10] No relays for loading reply message");
      return null;
    }

    const filter: Filter = {
      ids: [eventId],
      limit: 1,
    };

    const events = await firstValueFrom(
      pool.request(relays, [filter], { eventStore }).pipe(toArray()),
    );

    return events[0] || null;
  }

  /**
   * Get capabilities of NIP-10 protocol
   */
  getCapabilities(): ChatCapabilities {
    return {
      supportsEncryption: false,
      supportsThreading: true,
      supportsModeration: false,
      supportsRoles: false,
      supportsGroupManagement: false,
      canCreateConversations: false,
      requiresRelay: false,
    };
  }

  /**
   * Extract a readable title from root event content
   */
  private extractTitle(rootEvent: NostrEvent): string {
    const content = rootEvent.content.trim();
    if (!content) return `Thread by ${rootEvent.pubkey.slice(0, 8)}...`;

    // Try to get first line
    const firstLine = content.split("\n")[0];
    if (firstLine && firstLine.length <= 50) {
      return firstLine;
    }

    // Truncate to 50 chars
    if (content.length <= 50) {
      return content;
    }

    return content.slice(0, 47) + "...";
  }

  /**
   * Extract unique participants from thread
   */
  private extractParticipants(
    rootEvent: NostrEvent,
    providedEvent: NostrEvent,
  ): Participant[] {
    const participants = new Map<string, Participant>();

    // Root author is always first
    participants.set(rootEvent.pubkey, {
      pubkey: rootEvent.pubkey,
      role: "op", // Root author is "op" (original poster) of the thread
    });

    // Add p-tags from root event
    for (const tag of rootEvent.tags) {
      if (tag[0] === "p" && tag[1] && tag[1] !== rootEvent.pubkey) {
        participants.set(tag[1], {
          pubkey: tag[1],
          role: "member",
        });
      }
    }

    // Add provided event author (if different)
    if (providedEvent.pubkey !== rootEvent.pubkey) {
      participants.set(providedEvent.pubkey, {
        pubkey: providedEvent.pubkey,
        role: "member",
      });
    }

    // Add p-tags from provided event
    for (const tag of providedEvent.tags) {
      if (tag[0] === "p" && tag[1] && tag[1] !== providedEvent.pubkey) {
        participants.set(tag[1], {
          pubkey: tag[1],
          role: "member",
        });
      }
    }

    return Array.from(participants.values());
  }

  /**
   * Determine best relays for the thread
   * Includes relays from root author, provided event author, p-tagged participants, and active user
   */
  private async getThreadRelays(
    rootEvent: NostrEvent,
    providedEvent: NostrEvent,
    providedRelays: string[],
  ): Promise<string[]> {
    const relaySets: string[][] = [];

    // 1. Provided relay hints (highest priority)
    relaySets.push(providedRelays);

    // 2. Root author's outbox relays (NIP-65)
    try {
      const rootOutbox = await this.getOutboxRelays(rootEvent.pubkey);
      relaySets.push(rootOutbox.slice(0, 3));
    } catch (err) {
      console.warn("[NIP-10] Failed to get root author outbox:", err);
    }

    // 3. Collect unique participant pubkeys from both events' p-tags
    const participantPubkeys = new Set<string>();
    for (const tag of rootEvent.tags) {
      if (tag[0] === "p" && tag[1]) participantPubkeys.add(tag[1]);
    }
    for (const tag of providedEvent.tags) {
      if (tag[0] === "p" && tag[1]) participantPubkeys.add(tag[1]);
    }
    if (providedEvent.pubkey !== rootEvent.pubkey) {
      participantPubkeys.add(providedEvent.pubkey);
    }

    // 4. Fetch outbox relays from participant subset (limit to avoid slowdown)
    const participantsToCheck = Array.from(participantPubkeys).slice(0, 5);
    for (const pubkey of participantsToCheck) {
      try {
        const outbox = await this.getOutboxRelays(pubkey);
        if (outbox.length > 0) relaySets.push([outbox[0]]);
      } catch {
        // Silently continue if participant has no relay list
      }
    }

    // 5. Active user's outbox (for publishing replies)
    const activePubkey = accountManager.active$.value?.pubkey;
    if (activePubkey && !participantPubkeys.has(activePubkey)) {
      try {
        const userOutbox = await this.getOutboxRelays(activePubkey);
        relaySets.push(userOutbox.slice(0, 2));
      } catch (err) {
        console.warn("[NIP-10] Failed to get user outbox:", err);
      }
    }

    // Merge all relay sets (handles deduplication and normalization)
    let relays = mergeRelaySets(...relaySets);

    // 6. Fallback to aggregator relays if we have too few
    if (relays.length < 3) {
      relays = mergeRelaySets(relays, AGGREGATOR_RELAYS);
    }

    // Limit to 10 relays max for performance
    return relays.slice(0, 10);
  }

  /**
   * Helper: Get outbox relays for a pubkey (NIP-65)
   */
  private async getOutboxRelays(pubkey: string): Promise<string[]> {
    const relayList = await firstValueFrom(
      eventStore.replaceable(10002, pubkey, ""),
      { defaultValue: undefined },
    );

    if (!relayList) return [];

    // Use applesauce helper to extract write relays
    return getOutboxes(relayList).slice(0, 5);
  }

  /**
   * Helper: Fetch an event by ID from relays
   */
  private async fetchEvent(
    eventId: string,
    relayHints: string[] = [],
  ): Promise<NostrEvent | null> {
    // Check EventStore first
    const cached = await firstValueFrom(eventStore.event(eventId), {
      defaultValue: undefined,
    });
    if (cached) return cached;

    // Not in store - fetch from relays
    const relays =
      relayHints.length > 0 ? relayHints : await this.getDefaultRelays();

    const filter: Filter = {
      ids: [eventId],
      limit: 1,
    };

    const events: NostrEvent[] = [];
    const obs = pool.subscription(relays, [filter], { eventStore });

    await new Promise<void>((resolve) => {
      const timeout = setTimeout(() => {
        resolve();
      }, 5000);

      const sub = obs.subscribe({
        next: (response) => {
          if (typeof response === "string") {
            // EOSE received
            clearTimeout(timeout);
            sub.unsubscribe();
            resolve();
          } else {
            // Event received
            events.push(response);
          }
        },
        error: (err) => {
          clearTimeout(timeout);
          console.error(`[NIP-10] Fetch error:`, err);
          sub.unsubscribe();
          resolve();
        },
      });
    });

    return events[0] || null;
  }

  /**
   * Helper: Get default relays to use when no hints provided
   */
  private async getDefaultRelays(): Promise<string[]> {
    const activePubkey = accountManager.active$.value?.pubkey;
    if (activePubkey) {
      const outbox = await this.getOutboxRelays(activePubkey);
      if (outbox.length > 0) return outbox.slice(0, 5);
    }

    // Fallback to aggregator relays
    return AGGREGATOR_RELAYS;
  }

  /**
   * Convert root event to Message object
   */
  private rootEventToMessage(
    event: NostrEvent,
    conversationId: string,
    _rootEventId: string,
  ): Message | null {
    if (event.kind !== 1) {
      return null;
    }

    // Root event has no replyTo field
    return {
      id: event.id,
      conversationId,
      author: event.pubkey,
      content: event.content,
      timestamp: event.created_at,
      type: "user",
      replyTo: undefined,
      protocol: "nip-10",
      metadata: {
        encrypted: false,
      },
      event,
    };
  }

  /**
   * Convert Nostr event to Message object
   */
  private eventToMessage(
    event: NostrEvent,
    conversationId: string,
    rootEventId: string,
  ): Message | null {
    // Handle zap receipts (kind 9735)
    if (event.kind === 9735) {
      return this.zapToMessage(event, conversationId);
    }

    // Handle reposts (kind 6, 16) - simple system messages
    // Content is ignored even if present (quotes)
    if (event.kind === 6 || event.kind === 16) {
      return this.repostToMessage(event, conversationId);
    }

    // Handle reactions (kind 7) - skip for now, handled via MessageReactions
    if (event.kind === 7) {
      return null;
    }

    // Handle replies (kind 1)
    if (event.kind === 1) {
      const refs = getNip10References(event);

      // Determine what this reply is responding to (as full EventPointer with relay hints)
      let replyTo: EventPointer | undefined;

      if (refs.reply?.e) {
        // Replying to another reply - use the full EventPointer
        replyTo = refs.reply.e;
      } else if (refs.root?.e) {
        // Replying directly to root - use the full EventPointer
        replyTo = refs.root.e;
      } else {
        // Malformed or legacy reply - assume replying to root (no relay hints)
        replyTo = { id: rootEventId };
      }

      return {
        id: event.id,
        conversationId,
        author: event.pubkey,
        content: event.content,
        timestamp: event.created_at,
        type: "user",
        replyTo,
        protocol: "nip-10",
        metadata: {
          encrypted: false,
        },
        event,
      };
    }

    console.warn(`[NIP-10] Unknown event kind: ${event.kind}`);
    return null;
  }

  /**
   * Convert zap receipt to Message object
   */
  private zapToMessage(
    zapReceipt: NostrEvent,
    conversationId: string,
  ): Message {
    // Extract zap metadata using applesauce helpers
    const amount = getZapAmount(zapReceipt);
    const sender = getZapSender(zapReceipt);
    const recipient = getZapRecipient(zapReceipt);

    // Convert from msats to sats
    const amountInSats = amount ? Math.floor(amount / 1000) : 0;

    // Find what event is being zapped (e-tag in zap receipt) - use full pointer with relay hints
    const eTag = zapReceipt.tags.find((t) => t[0] === "e");
    const replyTo = eTag
      ? (getEventPointerFromETag(eTag) ?? undefined)
      : undefined;

    // Get zap request event for comment
    const zapRequestTag = zapReceipt.tags.find((t) => t[0] === "description");
    let comment = "";
    if (zapRequestTag && zapRequestTag[1]) {
      try {
        const zapRequest = JSON.parse(zapRequestTag[1]) as NostrEvent;
        comment = zapRequest.content || "";
      } catch {
        // Invalid JSON
      }
    }

    return {
      id: zapReceipt.id,
      conversationId,
      author: sender || zapReceipt.pubkey,
      content: comment,
      timestamp: zapReceipt.created_at,
      type: "zap",
      replyTo,
      protocol: "nip-10",
      metadata: {
        zapAmount: amountInSats,
        zapRecipient: recipient,
      },
      event: zapReceipt,
    };
  }

  /**
   * Convert repost event to system Message object
   */
  private repostToMessage(
    repostEvent: NostrEvent,
    conversationId: string,
  ): Message {
    // Find what event is being reposted (e-tag) - use full pointer with relay hints
    const eTag = repostEvent.tags.find((t) => t[0] === "e");
    const replyTo = eTag
      ? (getEventPointerFromETag(eTag) ?? undefined)
      : undefined;

    return {
      id: repostEvent.id,
      conversationId,
      author: repostEvent.pubkey,
      content: "reposted",
      timestamp: repostEvent.created_at,
      type: "system",
      replyTo,
      protocol: "nip-10",
      metadata: {},
      event: repostEvent,
    };
  }
}
