/**
 * Bluesky cross-posting helpers
 *
 * Builds app.bsky.feed.post records with link cards and rich text facets.
 */

import type { BlobRef } from "./atproto.js";

// ── Pre-compiled regexes ────────────────────────────────────────

const TEMPLATE_TITLE_RE = /\{title\}/g;
const TEMPLATE_URL_RE = /\{url\}/g;
const TEMPLATE_EXCERPT_RE = /\{excerpt\}/g;
const TRAILING_PUNCTUATION_RE = /[.,;:!?'"]+$/;
// Global regexes for facet detection -- reset lastIndex before each use
const URL_REGEX = /https?:\/\/[^\s)>\]]+/g;
const HASHTAG_REGEX = /(?<=\s|^)#([a-zA-Z0-9_]+)/g;

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

export interface BskyPost {
	$type: "app.bsky.feed.post";
	text: string;
	createdAt: string;
	langs?: string[];
	facets?: BskyFacet[];
	embed?: BskyEmbed;
}

export interface BskyFacet {
	index: { byteStart: number; byteEnd: number };
	features: Array<
		| { $type: "app.bsky.richtext.facet#link"; uri: string }
		| { $type: "app.bsky.richtext.facet#tag"; tag: string }
	>;
}

export type BskyEmbed = {
	$type: "app.bsky.embed.external";
	external: {
		uri: string;
		title: string;
		description: string;
		thumb?: BlobRef;
	};
};

// ── Post builder ────────────────────────────────────────────────

/**
 * Build a Bluesky post record for cross-posting published content.
 */
export function buildBskyPost(opts: {
	template: string;
	content: Record<string, unknown>;
	siteUrl: string;
	thumbBlob?: BlobRef;
	langs?: string[];
}): BskyPost {
	const { template, content, siteUrl, thumbBlob, langs } = opts;

	const title = (content.title as string) || "Untitled";
	const slug = content.slug as string;
	const excerpt = (content.excerpt || content.description || "") as string;
	const url = slug ? `${stripTrailingSlash(siteUrl)}/${slug}` : siteUrl;

	// Apply template -- substitute before truncation so we can detect
	// if the URL survives intact after truncation
	const fullText = template
		.replace(TEMPLATE_TITLE_RE, title)
		.replace(TEMPLATE_URL_RE, url)
		.replace(TEMPLATE_EXCERPT_RE, excerpt);

	// Truncate to 300 graphemes (Bluesky limit)
	const text = truncateGraphemes(fullText, 300);
	const wasTruncated = text !== fullText;

	const post: BskyPost = {
		$type: "app.bsky.feed.post",
		text,
		createdAt: new Date().toISOString(),
	};

	if (langs && langs.length > 0) {
		post.langs = langs.slice(0, 3); // Max 3 per spec
	}

	// Auto-detect URLs in text and build facets.
	// If text was truncated, skip facets -- truncation may have cut
	// a URL mid-string, producing a broken link facet.
	if (!wasTruncated) {
		const facets = buildFacets(text);
		if (facets.length > 0) {
			post.facets = facets;
		}
	}

	// Link card embed
	post.embed = {
		$type: "app.bsky.embed.external",
		external: {
			uri: url,
			title,
			description: truncateGraphemes(excerpt, 300),
			...(thumbBlob ? { thumb: thumbBlob } : {}),
		},
	};

	return post;
}

// ── Rich text facets ────────────────────────────────────────────

/**
 * Build rich text facets for URLs and hashtags in text.
 *
 * CRITICAL: Facet byte offsets use UTF-8 bytes, not JavaScript string indices.
 */
export function buildFacets(text: string): BskyFacet[] {
	const encoder = new TextEncoder();
	const facets: BskyFacet[] = [];

	// Detect URLs
	let match: RegExpExecArray | null;
	URL_REGEX.lastIndex = 0;
	while ((match = URL_REGEX.exec(text)) !== null) {
		// Strip trailing punctuation that was captured by the greedy regex
		const cleanUrl = match[0].replace(TRAILING_PUNCTUATION_RE, "");
		const beforeBytes = encoder.encode(text.slice(0, match.index));
		const matchBytes = encoder.encode(cleanUrl);
		facets.push({
			index: {
				byteStart: beforeBytes.length,
				byteEnd: beforeBytes.length + matchBytes.length,
			},
			features: [{ $type: "app.bsky.richtext.facet#link", uri: cleanUrl }],
		});
	}

	// Detect hashtags
	HASHTAG_REGEX.lastIndex = 0;
	while ((match = HASHTAG_REGEX.exec(text)) !== null) {
		const tag = match[1];
		if (!tag) continue;

		// Include the # in the byte range
		const beforeBytes = encoder.encode(text.slice(0, match.index));
		const matchBytes = encoder.encode(match[0]);
		facets.push({
			index: {
				byteStart: beforeBytes.length,
				byteEnd: beforeBytes.length + matchBytes.length,
			},
			features: [{ $type: "app.bsky.richtext.facet#tag", tag }],
		});
	}

	return facets;
}

// ── Utilities ───────────────────────────────────────────────────

/**
 * Truncate a string to a maximum number of graphemes.
 * Uses Intl.Segmenter for correct Unicode handling.
 */
function truncateGraphemes(text: string, maxGraphemes: number): string {
	// Intl.Segmenter handles multi-codepoint graphemes (emoji, combining chars)
	const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
	const segments = [...segmenter.segment(text)];

	if (segments.length <= maxGraphemes) return text;

	// Truncate and add ellipsis
	return (
		segments
			.slice(0, maxGraphemes - 1)
			.map((s) => s.segment)
			.join("") + "\u2026"
	);
}

function stripTrailingSlash(url: string): string {
	return url.endsWith("/") ? url.slice(0, -1) : url;
}
