import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared";
import {
  NostrPrefix,
  decodeTLV,
  TLVEntryType,
  encodeTLV,
  NostrEvent,
  TaggedNostrEvent,
  EventExt,
  Tag,
  EventKind,
} from ".";
import { findTag } from "./utils";

export class NostrLink {
  constructor(
    readonly type: NostrPrefix,
    readonly id: string,
    readonly kind?: number,
    readonly author?: string,
    readonly relays?: Array<string>,
  ) {}

  encode(): string {
    if (this.type === NostrPrefix.Note || this.type === NostrPrefix.PrivateKey || this.type === NostrPrefix.PublicKey) {
      return hexToBech32(this.type, this.id);
    } else {
      return encodeTLV(this.type, this.id, this.relays, this.kind, this.author);
    }
  }

  toEventTag() {
    const relayEntry = this.relays ? [this.relays[0]] : [];
    if (this.type === NostrPrefix.PublicKey) {
      return ["p", this.id];
    } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) {
      return ["e", this.id, ...relayEntry];
    } else if (this.type === NostrPrefix.Address) {
      return ["a", `${this.kind}:${this.author}:${this.id}`, ...relayEntry];
    }
  }

  matchesEvent(ev: NostrEvent) {
    if (this.type === NostrPrefix.Address) {
      const dTag = findTag(ev, "d");
      if (dTag && dTag === this.id && unwrap(this.author) === ev.pubkey && unwrap(this.kind) === ev.kind) {
        return true;
      }
    } else if (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note) {
      return this.id === ev.id;
    }

    return false;
  }

  /**
   * Is the supplied event a reply to this link
   */
  isReplyToThis(ev: NostrEvent) {
    const NonNip10Kinds = [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
    if (NonNip10Kinds.includes(ev.kind)) {
      const lastRef = ev.tags.findLast(a => a[0] === "e" || a[1] === "a");
      if (!lastRef) return false;

      if (
        lastRef[0] === "e" &&
        lastRef[1] === this.id &&
        (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note)
      ) {
        return true;
      }
      if (lastRef[0] === "a" && this.type === NostrPrefix.Address) {
        const [kind, author, dTag] = lastRef[1].split(":");
        if (Number(kind) === this.kind && author === this.author && dTag === this.id) {
          return true;
        }
      }
    } else {
      const thread = EventExt.extractThread(ev);
      if (!thread) return false; // non-thread events are not replies

      if (!thread.root) return false; // must have root marker or positional e/a tag in position 0

      if (
        thread.root.key === "e" &&
        thread.root.value === this.id &&
        (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note)
      ) {
        return true;
      }
      if (thread.root.key === "a" && this.type === NostrPrefix.Address) {
        const [kind, author, dTag] = unwrap(thread.root.value).split(":");
        if (Number(kind) === this.kind && author === this.author && dTag === this.id) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Does the supplied event contain a tag matching this link
   */
  referencesThis(ev: NostrEvent) {
    for (const t of ev.tags) {
      if (t[0] === "e" && t[1] === this.id && (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note)) {
        return true;
      }
      if (t[0] === "a" && this.type === NostrPrefix.Address) {
        const [kind, author, dTag] = t[1].split(":");
        if (Number(kind) === this.kind && author === this.author && dTag === this.id) {
          return true;
        }
      }
      if (
        t[0] === "p" &&
        (this.type === NostrPrefix.Profile || this.type === NostrPrefix.PublicKey) &&
        this.id === t[1]
      ) {
        return true;
      }
    }
    return false;
  }

  equals(other: NostrLink) {
    if (other.type === this.type && this.type === NostrPrefix.Address) {
    }
  }

  static fromThreadTag(tag: Tag) {
    const relay = tag.relay ? [tag.relay] : undefined;

    switch (tag.key) {
      case "e": {
        return new NostrLink(NostrPrefix.Event, unwrap(tag.value), undefined, undefined, relay);
      }
      case "p": {
        return new NostrLink(NostrPrefix.Profile, unwrap(tag.value), undefined, undefined, relay);
      }
      case "a": {
        const [kind, author, dTag] = unwrap(tag.value).split(":");
        return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relay);
      }
    }
    throw new Error(`Unknown tag kind ${tag.key}`);
  }

  static fromTag(tag: Array<string>) {
    const relays = tag.length > 2 ? [tag[2]] : undefined;
    switch (tag[0]) {
      case "e": {
        return new NostrLink(NostrPrefix.Event, tag[1], undefined, undefined, relays);
      }
      case "p": {
        return new NostrLink(NostrPrefix.Profile, tag[1], undefined, undefined, relays);
      }
      case "a": {
        const [kind, author, dTag] = tag[1].split(":");
        return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays);
      }
    }
    throw new Error(`Unknown tag kind ${tag[0]}`);
  }

  static fromEvent(ev: TaggedNostrEvent | NostrEvent) {
    const relays = "relays" in ev ? ev.relays : undefined;

    if (ev.kind >= 30_000 && ev.kind < 40_000) {
      const dTag = unwrap(findTag(ev, "d"));
      return new NostrLink(NostrPrefix.Address, dTag, ev.kind, ev.pubkey, relays);
    }
    return new NostrLink(NostrPrefix.Event, ev.id, ev.kind, ev.pubkey, relays);
  }
}

export function validateNostrLink(link: string): boolean {
  try {
    const parsedLink = parseNostrLink(link);
    if (!parsedLink) {
      return false;
    }
    if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) {
      return parsedLink.id.length === 64;
    }

    return true;
  } catch {
    return false;
  }
}

export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined {
  try {
    return parseNostrLink(link, prefixHint);
  } catch {
    return undefined;
  }
}

export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink {
  const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link;

  const isPrefix = (prefix: NostrPrefix) => {
    return entity.startsWith(prefix);
  };

  if (isPrefix(NostrPrefix.PublicKey)) {
    const id = bech32ToHex(entity);
    if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
    return new NostrLink(NostrPrefix.PublicKey, id);
  } else if (isPrefix(NostrPrefix.Note)) {
    const id = bech32ToHex(entity);
    if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
    return new NostrLink(NostrPrefix.Note, id);
  } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
    const decoded = decodeTLV(entity);

    const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string;
    const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string);
    const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string;
    const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number;

    if (isPrefix(NostrPrefix.Profile)) {
      if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
      return new NostrLink(NostrPrefix.Profile, id, kind, author, relays);
    } else if (isPrefix(NostrPrefix.Event)) {
      if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
      return new NostrLink(NostrPrefix.Event, id, kind, author, relays);
    } else if (isPrefix(NostrPrefix.Address)) {
      return new NostrLink(NostrPrefix.Address, id, kind, author, relays);
    }
  } else if (prefixHint) {
    return new NostrLink(prefixHint, link);
  }
  throw new Error("Invalid nostr link");
}
