/**
 * Event signing guardrails - individual check functions
 */

import type { NDKEvent } from "../../events/index.js";

type ErrorFn = (id: string, message: string, hint?: string, canDisable?: boolean) => never | undefined;
type WarnFn = (id: string, message: string, hint?: string) => never | undefined;

/**
 * Check that event has a kind field
 */
function checkMissingKind(event: NDKEvent, error: ErrorFn): void {
    if (event.kind === undefined || event.kind === null) {
        error(
            "event-missing-kind",
            `Cannot sign event without 'kind'.\n\n` +
                `📦 Event data:\n` +
                `   • content: ${event.content ? `"${event.content.substring(0, 50)}${event.content.length > 50 ? "..." : ""}"` : "(empty)"}\n` +
                `   • tags: ${event.tags.length} tag${event.tags.length !== 1 ? "s" : ""}\n` +
                `   • kind: ${event.kind} ❌\n\n` +
                `Set event.kind before signing.`,
            "Example: event.kind = 1; // for text note",
            false, // Fatal error - cannot be disabled
        );
    }
}

/**
 * Check that content is a string, not an object
 */
function checkContentIsObject(event: NDKEvent, error: ErrorFn): void {
    if (typeof event.content === "object") {
        const contentPreview = JSON.stringify(event.content, null, 2).substring(0, 200);
        error(
            "event-content-is-object",
            `Event content is an object. Content must be a string.\n\n` +
                `📦 Your content (${typeof event.content}):\n${contentPreview}${JSON.stringify(event.content).length > 200 ? "..." : ""}\n\n` +
                `❌ event.content = { ... }  // WRONG\n` +
                `✅ event.content = JSON.stringify({ ... })  // CORRECT`,
            "Use JSON.stringify() for structured data: event.content = JSON.stringify(data)",
            false, // Fatal error - cannot be disabled
        );
    }
}

/**
 * Check that created_at is in seconds, not milliseconds
 */
function checkCreatedAtMilliseconds(event: NDKEvent, error: ErrorFn): void {
    if (event.created_at && event.created_at > 10000000000) {
        const correctValue = Math.floor(event.created_at / 1000);
        const dateString = new Date(event.created_at).toISOString();
        error(
            "event-created-at-milliseconds",
            `Event created_at is in milliseconds, not seconds.\n\n` +
                `📦 Your value:\n` +
                `   • created_at: ${event.created_at} ❌\n` +
                `   • Interpreted as: ${dateString}\n` +
                `   • Should be: ${correctValue} ✅\n\n` +
                `Nostr timestamps MUST be in seconds since Unix epoch.`,
            "Use Math.floor(Date.now() / 1000) instead of Date.now()",
            false, // Fatal error - cannot be disabled
        );
    }
}

/**
 * Check that p-tags contain valid hex pubkeys
 */
function checkInvalidPTags(event: NDKEvent, error: ErrorFn): void {
    const pTags = event.getMatchingTags("p");
    pTags.forEach((tag, idx) => {
        if (tag[1] && !/^[0-9a-f]{64}$/i.test(tag[1])) {
            const tagPreview = JSON.stringify(tag);
            error(
                "tag-invalid-p-tag",
                `p-tag[${idx}] has invalid pubkey.\n\n` +
                    `📦 Your tag:\n   ${tagPreview}\n\n` +
                    `❌ Invalid value: "${tag[1]}"\n` +
                    `   • Length: ${tag[1].length} (expected 64)\n` +
                    `   • Format: ${tag[1].startsWith("npub") ? "bech32 (npub)" : "unknown"}\n\n` +
                    `p-tags MUST contain 64-character hex pubkeys.`,
                tag[1].startsWith("npub")
                    ? "Use ndkUser.pubkey instead of npub:\n   ✅ event.tags.push(['p', ndkUser.pubkey])\n   ❌ event.tags.push(['p', 'npub1...'])"
                    : "p-tags must contain valid hex pubkeys (64 characters, 0-9a-f)",
                false, // Fatal error - cannot be disabled
            );
        }
    });
}

/**
 * Check that e-tags contain valid hex event IDs
 */
function checkInvalidETags(event: NDKEvent, error: ErrorFn): void {
    const eTags = event.getMatchingTags("e");
    eTags.forEach((tag, idx) => {
        if (tag[1] && !/^[0-9a-f]{64}$/i.test(tag[1])) {
            const tagPreview = JSON.stringify(tag);
            const isBech32 = tag[1].startsWith("note") || tag[1].startsWith("nevent");
            error(
                "tag-invalid-e-tag",
                `e-tag[${idx}] has invalid event ID.\n\n` +
                    `📦 Your tag:\n   ${tagPreview}\n\n` +
                    `❌ Invalid value: "${tag[1]}"\n` +
                    `   • Length: ${tag[1].length} (expected 64)\n` +
                    `   • Format: ${isBech32 ? "bech32 (note/nevent)" : "unknown"}\n\n` +
                    `e-tags MUST contain 64-character hex event IDs.`,
                isBech32
                    ? "Use event.id instead of bech32:\n   ✅ event.tags.push(['e', referencedEvent.id])\n   ❌ event.tags.push(['e', 'note1...'])"
                    : "e-tags must contain valid hex event IDs (64 characters, 0-9a-f)",
                false, // Fatal error - cannot be disabled
            );
        }
    });
}

/**
 * Check for manual reply markers (should use .reply() instead)
 */
function checkManualReplyMarkers(event: NDKEvent, warn: WarnFn, replyEvents: WeakSet<NDKEvent>): void {
    if (event.kind !== 1) return;

    // If this event was created via .reply(), skip the check
    if (replyEvents.has(event)) return;

    const eTagsWithMarkers = event.tags.filter((tag) => tag[0] === "e" && (tag[3] === "reply" || tag[3] === "root"));

    if (eTagsWithMarkers.length > 0) {
        const tagList = eTagsWithMarkers.map((tag, idx) => `   ${idx + 1}. ${JSON.stringify(tag)}`).join("\n");
        warn(
            "event-manual-reply-markers",
            `Event has ${eTagsWithMarkers.length} e-tag(s) with manual reply/root markers.\n\n` +
                `📦 Your tags with markers:\n${tagList}\n\n` +
                `⚠️  Manual reply markers detected! This will cause incorrect threading.`,
            `Reply events MUST be created using .reply():\n\n` +
                `   ✅ CORRECT:\n` +
                `   const replyEvent = originalEvent.reply();\n` +
                `   replyEvent.content = 'good point!';\n` +
                `   await replyEvent.publish();\n\n` +
                `   ❌ WRONG:\n` +
                `   event.tags.push(['e', eventId, '', 'reply']);\n\n` +
                `NDK handles all reply threading automatically - never add reply/root markers manually.`,
        );
    }
}

/**
 * Check that hashtag tags don't include the # prefix
 */
function checkHashtagsWithPrefix(event: NDKEvent, error: ErrorFn): void {
    const tTags = event.getMatchingTags("t");
    tTags.forEach((tag, idx) => {
        if (tag[1] && tag[1].startsWith("#")) {
            const tagPreview = JSON.stringify(tag);
            error(
                "tag-hashtag-with-prefix",
                `t-tag[${idx}] contains hashtag with # prefix.\n\n` +
                    `📦 Your tag:\n   ${tagPreview}\n\n` +
                    `❌ Invalid value: "${tag[1]}"\n\n` +
                    `Hashtag tags should NOT include the # symbol.`,
                `Remove the # prefix from hashtag tags:\n   ✅ event.tags.push(['t', 'nostr'])\n   ❌ event.tags.push(['t', '#nostr'])`,
                false, // Fatal error - cannot be disabled
            );
        }
    });
}

/**
 * Check that replaceable events use publishReplaceable() when modified
 */
function checkReplaceableWithOldTimestamp(event: NDKEvent, warn: WarnFn): void {
    if (event.kind === undefined || event.kind === null || !event.created_at) return;
    if (!event.isReplaceable()) return;

    const nowSeconds = Math.floor(Date.now() / 1000);
    const ageSeconds = nowSeconds - event.created_at;
    const TEN_SECONDS = 10;

    if (ageSeconds > TEN_SECONDS) {
        const ageMinutes = Math.floor(ageSeconds / 60);
        const ageDescription = ageMinutes > 0 ? `${ageMinutes} minute${ageMinutes !== 1 ? "s" : ""}` : `${ageSeconds} seconds`;

        warn(
            "event-replaceable-old-timestamp",
            `Publishing a replaceable event with an old created_at timestamp.\n\n` +
                `📦 Event details:\n` +
                `   • kind: ${event.kind} (replaceable)\n` +
                `   • created_at: ${event.created_at}\n` +
                `   • age: ${ageDescription} old\n` +
                `   • current time: ${nowSeconds}\n\n` +
                `⚠️  This is wrong and will be rejected by relays.`,
            `For replaceable events, use publishReplaceable():\n\n` +
                `   ✅ CORRECT:\n` +
                `   await event.publishReplaceable();\n` +
                `   // Automatically updates created_at to now\n\n` +
                `   ❌ WRONG:\n` +
                `   await event.publish();\n` +
                `   // Uses old created_at`,
        );
    }
}

/**
 * Called when an event is about to be signed.
 * Runs all signing-related checks.
 */
export function signing(event: NDKEvent, error: ErrorFn, warn: WarnFn, replyEvents: WeakSet<NDKEvent>): void {
    checkMissingKind(event, error);
    checkContentIsObject(event, error);
    checkCreatedAtMilliseconds(event, error);
    checkInvalidPTags(event, error);
    checkInvalidETags(event, error);
    checkHashtagsWithPrefix(event, error);
    checkManualReplyMarkers(event, warn, replyEvents);
}

/**
 * Called when an event is about to be published.
 * Runs all publishing-related checks.
 */
export function publishing(event: NDKEvent, warn: WarnFn): void {
    checkReplaceableWithOldTimestamp(event, warn);
}
