import fs from "node:fs/promises";
import path from "node:path";
import { Type } from "@sinclair/typebox";

//#region extensions/nitter/src/config.ts
function asString(v, fallback) {
	return typeof v === "string" && v.trim() ? v.trim() : fallback;
}
function asNumber(v, fallback) {
	return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
}
function asBool(v, fallback) {
	return typeof v === "boolean" ? v : fallback;
}
function parseDeliveryMode(v) {
	if (v === "session" || v === "channel" || v === "both") return v;
	return "both";
}
function parseFeedConfig(raw) {
	if (!raw || typeof raw !== "object") return null;
	const obj = raw;
	const username = typeof obj.username === "string" ? obj.username.trim() : "";
	if (!username) return null;
	return {
		username,
		label: typeof obj.label === "string" ? obj.label.trim() || void 0 : void 0,
		agentId: typeof obj.agentId === "string" ? obj.agentId.trim() || void 0 : void 0,
		pollIntervalMs: typeof obj.pollIntervalMs === "number" && obj.pollIntervalMs > 0 ? obj.pollIntervalMs : void 0,
		deliveryChannel: typeof obj.deliveryChannel === "string" ? obj.deliveryChannel.trim() || void 0 : void 0,
		deliveryTo: typeof obj.deliveryTo === "string" ? obj.deliveryTo.trim() || void 0 : void 0,
		filter: typeof obj.filter === "string" ? obj.filter.trim() || void 0 : void 0,
		enabled: asBool(obj.enabled, true)
	};
}
function parseNitterConfig(raw) {
	const obj = raw && typeof raw === "object" ? raw : {};
	const feeds = [];
	if (Array.isArray(obj.feeds)) for (const entry of obj.feeds) {
		const parsed = parseFeedConfig(entry);
		if (parsed) feeds.push(parsed);
	}
	return {
		enabled: asBool(obj.enabled, true),
		instanceUrl: asString(obj.instanceUrl, "http://localhost:8080").replace(/\/+$/, ""),
		pollIntervalMs: asNumber(obj.pollIntervalMs, 3e5),
		maxItemsPerPoll: asNumber(obj.maxItemsPerPoll, 20),
		requestTimeoutMs: asNumber(obj.requestTimeoutMs, 15e3),
		deliveryChannel: typeof obj.deliveryChannel === "string" ? obj.deliveryChannel.trim() || void 0 : void 0,
		deliveryTo: typeof obj.deliveryTo === "string" ? obj.deliveryTo.trim() || void 0 : void 0,
		deliveryMode: parseDeliveryMode(obj.deliveryMode),
		feeds
	};
}

//#endregion
//#region extensions/nitter/src/delivery.ts
function formatRelativeTime(ms) {
	const diff = Date.now() - ms;
	if (diff < 0) return "just now";
	const minutes = Math.floor(diff / 6e4);
	if (minutes < 1) return "just now";
	if (minutes < 60) return `${minutes}m ago`;
	const hours = Math.floor(minutes / 60);
	if (hours < 24) return `${hours}h ago`;
	const days = Math.floor(hours / 24);
	if (days < 30) return `${days}d ago`;
	const months = Math.floor(days / 30);
	if (months < 12) return `${months}mo ago`;
	return `${Math.floor(months / 12)}y ago`;
}
function formatFeedDigest(items, subscription) {
	const { username, label } = subscription;
	const header = label ? `[Nitter: ${label} (@${username})]` : `[Nitter: @${username}]`;
	const displayItems = items.slice(0, 10);
	const lines = [header, ""];
	for (const item of displayItems) {
		const prefix = item.isRetweet ? "RT " : item.isReply ? "↩ " : "";
		const content = item.content.length > 280 ? `${item.content.slice(0, 280)}...` : item.content;
		const timestamp = formatRelativeTime(item.pubDateMs);
		lines.push(`${prefix}${content}`);
		lines.push(`${item.link} (${timestamp})`);
		lines.push("");
	}
	if (items.length > 10) lines.push(`${items.length} new tweet(s)`);
	return lines.join("\n").trimEnd();
}
function deliverFeedItems(params) {
	const { items, subscription, config, api, sessionKey } = params;
	const result = {
		sessionDelivered: false,
		channelDelivered: false,
		itemCount: items.length
	};
	if (items.length === 0) return result;
	const digest = formatFeedDigest(items, subscription);
	if ((config.deliveryMode === "session" || config.deliveryMode === "both") && sessionKey) {
		api.enqueueSystemEvent(digest, {
			sessionKey,
			contextKey: "nitter-feed"
		});
		result.sessionDelivered = true;
	}
	if (config.deliveryMode === "channel" || config.deliveryMode === "both" && !result.sessionDelivered) {
		const channel = subscription.deliveryChannel ?? config.deliveryChannel;
		const to = subscription.deliveryTo ?? config.deliveryTo;
		if (channel && to) {
			api.logger.info(`[nitter] channel delivery: ${channel} -> ${to} (${items.length} items)`);
			result.channelDelivered = true;
		} else api.logger.warn(`[nitter] channel delivery skipped: missing channel or to for @${subscription.username}`);
	}
	return result;
}

//#endregion
//#region extensions/nitter/src/feed-state.ts
const MAX_SEEN_IDS = 200;
function defaultState() {
	return {
		version: 1,
		subscriptions: [],
		pollState: {}
	};
}
function defaultPollState() {
	return {
		lastPollAtMs: 0,
		seenIds: [],
		consecutiveErrors: 0
	};
}
function keyOf(username) {
	return username.toLowerCase();
}
var FeedStateManager = class {
	constructor(stateDir) {
		this.state = defaultState();
		this.savePromise = null;
		this.filePath = path.join(stateDir, "plugins", "nitter", "feed-state.json");
	}
	async load() {
		try {
			const raw = await fs.readFile(this.filePath, "utf-8");
			const parsed = JSON.parse(raw);
			if (parsed?.version === 1 && Array.isArray(parsed.subscriptions)) this.state = {
				version: 1,
				subscriptions: parsed.subscriptions,
				pollState: parsed.pollState && typeof parsed.pollState === "object" ? parsed.pollState : {}
			};
			else this.state = defaultState();
		} catch {
			this.state = defaultState();
		}
	}
	async save() {
		const dir = path.dirname(this.filePath);
		await fs.mkdir(dir, { recursive: true });
		await fs.writeFile(this.filePath, JSON.stringify(this.state, null, 2) + "\n", "utf-8");
	}
	addSubscription(sub) {
		const existing = this.state.subscriptions.find((s) => keyOf(s.username) === keyOf(sub.username));
		if (existing) return existing;
		const full = {
			...sub,
			addedAtMs: Date.now()
		};
		this.state.subscriptions.push(full);
		this.save();
		return full;
	}
	removeSubscription(username) {
		const key = keyOf(username);
		const idx = this.state.subscriptions.findIndex((s) => keyOf(s.username) === key);
		if (idx === -1) return false;
		this.state.subscriptions.splice(idx, 1);
		delete this.state.pollState[key];
		this.save();
		return true;
	}
	listSubscriptions() {
		return this.state.subscriptions;
	}
	getSubscription(username) {
		return this.state.subscriptions.find((s) => keyOf(s.username) === keyOf(username));
	}
	getPollState(username) {
		const key = keyOf(username);
		return this.state.pollState[key] ?? defaultPollState();
	}
	recordPoll(username, result) {
		const key = keyOf(username);
		const ps = this.state.pollState[key] ?? defaultPollState();
		ps.lastPollAtMs = Date.now();
		if (result.error) {
			ps.consecutiveErrors += 1;
			ps.lastError = result.error;
			this.state.pollState[key] = ps;
			this.save();
			return [];
		}
		ps.consecutiveErrors = 0;
		ps.lastError = void 0;
		const items = result.items ?? [];
		const seenSet = new Set(ps.seenIds);
		const newItems = items.filter((item) => !seenSet.has(item.id));
		for (const item of newItems) ps.seenIds.push(item.id);
		if (ps.seenIds.length > MAX_SEEN_IDS) ps.seenIds = ps.seenIds.slice(ps.seenIds.length - MAX_SEEN_IDS);
		if (newItems.length > 0) ps.lastItemId = newItems[newItems.length - 1].id;
		this.state.pollState[key] = ps;
		this.save();
		return newItems;
	}
	syncFromConfig(configFeeds) {
		const configKeys = new Set(configFeeds.map((f) => keyOf(f.username)));
		this.state.subscriptions = this.state.subscriptions.filter((sub) => {
			if (sub.source === "tool") return true;
			return configKeys.has(keyOf(sub.username));
		});
		for (const feed of configFeeds) {
			const key = keyOf(feed.username);
			const idx = this.state.subscriptions.findIndex((s) => keyOf(s.username) === key);
			const entry = {
				username: feed.username,
				label: feed.label,
				agentId: feed.agentId,
				pollIntervalMs: feed.pollIntervalMs,
				deliveryChannel: feed.deliveryChannel,
				deliveryTo: feed.deliveryTo,
				filter: feed.filter,
				enabled: feed.enabled,
				addedAtMs: idx !== -1 ? this.state.subscriptions[idx].addedAtMs : Date.now(),
				source: "config"
			};
			if (idx !== -1) this.state.subscriptions[idx] = entry;
			else this.state.subscriptions.push(entry);
		}
		this.save();
	}
};

//#endregion
//#region extensions/nitter/src/nitter-tool.ts
const NitterToolParams = Type.Object({
	action: Type.Union([
		Type.Literal("subscribe"),
		Type.Literal("unsubscribe"),
		Type.Literal("list"),
		Type.Literal("fetch"),
		Type.Literal("status")
	], { description: "Action to perform" }),
	username: Type.Optional(Type.String({ description: "Twitter/X username (without @)" })),
	label: Type.Optional(Type.String({ description: "Human-friendly label for the feed" })),
	filter: Type.Optional(Type.String({ description: "Regex filter for tweet content" })),
	pollIntervalMs: Type.Optional(Type.Number({ description: "Custom poll interval in ms" })),
	deliveryChannel: Type.Optional(Type.String({ description: "Override delivery channel" })),
	deliveryTo: Type.Optional(Type.String({ description: "Override delivery target" }))
});
function requireUsername(params) {
	const username = typeof params.username === "string" ? params.username.trim().replace(/^@/, "") : "";
	if (!username) throw new Error("username is required for this action");
	return username;
}
function textResult(text) {
	return { content: [{
		type: "text",
		text
	}] };
}
function createNitterTool(deps) {
	const { config, getFeedState, fetchRss } = deps;
	return {
		name: "nitter_feed",
		label: "Nitter Feed Monitor",
		description: "Manage Nitter/Twitter RSS feed subscriptions. Subscribe to Twitter users, list active feeds, fetch tweets on demand, or check instance status.",
		parameters: NitterToolParams,
		async execute(_id, params) {
			const action = typeof params.action === "string" ? params.action.trim() : "";
			if (action === "subscribe") {
				const username = requireUsername(params);
				const feedState = getFeedState();
				if (!feedState) return textResult("Feed state manager is not available. The poller service may not have started yet.");
				const sub = feedState.addSubscription({
					username,
					label: typeof params.label === "string" ? params.label.trim() || void 0 : void 0,
					filter: typeof params.filter === "string" ? params.filter.trim() || void 0 : void 0,
					pollIntervalMs: typeof params.pollIntervalMs === "number" && params.pollIntervalMs > 0 ? params.pollIntervalMs : void 0,
					deliveryChannel: typeof params.deliveryChannel === "string" ? params.deliveryChannel.trim() || void 0 : void 0,
					deliveryTo: typeof params.deliveryTo === "string" ? params.deliveryTo.trim() || void 0 : void 0,
					enabled: true,
					source: "tool"
				});
				return textResult([
					`Subscribed to @${sub.username}`,
					sub.label ? `Label: ${sub.label}` : null,
					sub.filter ? `Filter: ${sub.filter}` : null,
					sub.pollIntervalMs ? `Poll interval: ${Math.round(sub.pollIntervalMs / 1e3)}s` : null,
					sub.deliveryChannel ? `Delivery channel: ${sub.deliveryChannel}` : null,
					sub.deliveryTo ? `Delivery target: ${sub.deliveryTo}` : null
				].filter(Boolean).join("\n"));
			}
			if (action === "unsubscribe") {
				const username = requireUsername(params);
				const feedState = getFeedState();
				if (!feedState) return textResult("Feed state manager is not available. The poller service may not have started yet.");
				if (!feedState.removeSubscription(username)) return textResult(`No subscription found for @${username}.`);
				return textResult(`Unsubscribed from @${username}.`);
			}
			if (action === "list") {
				const feedState = getFeedState();
				if (!feedState) return textResult("Feed state manager is not available. The poller service may not have started yet.");
				const subs = feedState.listSubscriptions();
				if (subs.length === 0) return textResult("No feed subscriptions.");
				const lines = [`Feed subscriptions (${subs.length}):\n`];
				for (const sub of subs) {
					const state = feedState.getPollState(sub.username);
					const status = sub.enabled ? "enabled" : "disabled";
					const label = sub.label ? ` (${sub.label})` : "";
					const lastPoll = state.lastPollAtMs ? `${Math.round((Date.now() - state.lastPollAtMs) / 1e3)}s ago` : "never";
					const errors = state.consecutiveErrors > 0 ? ` [${state.consecutiveErrors} errors]` : "";
					const seenCount = state.seenIds.length;
					lines.push(`@${sub.username}${label} [${status}]`, `  Last poll: ${lastPoll}${errors}`, `  Seen tweets: ${seenCount}`, `  Source: ${sub.source}`, "");
				}
				return textResult(lines.join("\n").trimEnd());
			}
			if (action === "fetch") {
				const username = requireUsername(params);
				const result = await fetchRss({
					instanceUrl: config.instanceUrl,
					username,
					maxItems: config.maxItemsPerPoll,
					timeoutMs: config.requestTimeoutMs
				});
				if (!result.ok) return textResult(`Failed to fetch RSS for @${username}: ${result.error}`);
				if (result.items.length === 0) return textResult(`No tweets found for @${username}.`);
				const lines = [`Recent tweets from @${username} (${result.items.length}):\n`];
				for (const item of result.items) {
					const prefix = item.isRetweet ? "RT " : item.isReply ? "Reply " : "";
					const content = item.content.length > 280 ? `${item.content.slice(0, 280)}...` : item.content;
					lines.push(`${prefix}${content}`);
					lines.push(`${item.link} (${item.pubDate})`);
					if (item.mediaUrls.length > 0) lines.push(`Media: ${item.mediaUrls.join(", ")}`);
					lines.push("");
				}
				return textResult(lines.join("\n").trimEnd());
			}
			if (action === "status") {
				const feedState = getFeedState();
				const subs = feedState?.listSubscriptions() ?? [];
				const enabledCount = subs.filter((s) => s.enabled).length;
				let instanceStatus;
				try {
					const res = await globalThis.fetch(config.instanceUrl, { signal: AbortSignal.timeout(5e3) });
					instanceStatus = res.ok ? `reachable (HTTP ${res.status})` : `error (HTTP ${res.status})`;
				} catch (err) {
					instanceStatus = `unreachable (${err instanceof Error ? err.message : String(err)})`;
				}
				return textResult([
					"Nitter Feed Monitor Status",
					"",
					`Instance: ${config.instanceUrl}`,
					`Instance status: ${instanceStatus}`,
					`Delivery mode: ${config.deliveryMode}`,
					`Poll interval: ${Math.round(config.pollIntervalMs / 1e3)}s`,
					`Request timeout: ${Math.round(config.requestTimeoutMs / 1e3)}s`,
					`Max items per poll: ${config.maxItemsPerPoll}`,
					`Active feeds: ${enabledCount}/${subs.length}`,
					`Feed state: ${feedState ? "loaded" : "not available"}`
				].join("\n"));
			}
			return textResult(`Unknown action: ${action}. Valid actions: subscribe, unsubscribe, list, fetch, status.`);
		}
	};
}

//#endregion
//#region extensions/nitter/src/poller-service.ts
const STARTUP_DELAY_MS = 1e4;
const TICK_INTERVAL_MS = 3e4;
let activeFeedState = null;
function getActiveFeedState() {
	return activeFeedState;
}
function createNitterPollerService(deps) {
	let interval = null;
	let startupTimeout = null;
	async function pollAllFeeds(feedState) {
		const subscriptions = feedState.listSubscriptions();
		for (const sub of subscriptions) {
			if (!sub.enabled) continue;
			try {
				const pollState = feedState.getPollState(sub.username);
				const pollIntervalMs = sub.pollIntervalMs ?? deps.config.pollIntervalMs;
				if (Date.now() - pollState.lastPollAtMs < pollIntervalMs) continue;
				const result = await deps.fetchRss({
					instanceUrl: deps.config.instanceUrl,
					username: sub.username,
					maxItems: deps.config.maxItemsPerPoll,
					timeoutMs: deps.config.requestTimeoutMs
				});
				if (result.ok) {
					const newItems = feedState.recordPoll(sub.username, { items: result.items });
					if (newItems.length > 0) {
						deps.deliverItems({
							items: newItems,
							subscription: sub,
							config: deps.config,
							api: deps.api
						});
						deps.api.logger.info(`[nitter] @${sub.username}: ${newItems.length} new item(s) delivered`);
					}
				} else {
					feedState.recordPoll(sub.username, { error: result.error });
					deps.api.logger.warn(`[nitter] @${sub.username} poll failed: ${result.error}`);
				}
			} catch (err) {
				const message = err instanceof Error ? err.message : String(err);
				feedState.recordPoll(sub.username, { error: message });
				deps.api.logger.warn(`[nitter] @${sub.username} poll error: ${message}`);
			}
		}
	}
	return {
		id: "nitter-poller",
		async start(ctx) {
			const feedState = new deps.FeedStateManager(ctx.stateDir);
			await feedState.load();
			feedState.syncFromConfig(deps.config.feeds);
			activeFeedState = feedState;
			startupTimeout = setTimeout(() => {
				startupTimeout = null;
				interval = setInterval(() => {
					pollAllFeeds(feedState);
				}, TICK_INTERVAL_MS);
				interval.unref?.();
				pollAllFeeds(feedState);
			}, STARTUP_DELAY_MS);
			startupTimeout.unref?.();
		},
		stop() {
			if (startupTimeout !== null) {
				clearTimeout(startupTimeout);
				startupTimeout = null;
			}
			if (interval !== null) {
				clearInterval(interval);
				interval = null;
			}
			activeFeedState = null;
		}
	};
}

//#endregion
//#region extensions/nitter/src/rss.ts
const ENTITY_MAP = {
	"&amp;": "&",
	"&lt;": "<",
	"&gt;": ">",
	"&quot;": "\"",
	"&#39;": "'",
	"&#x27;": "'",
	"&#x2F;": "/",
	"&apos;": "'"
};
const ENTITY_RE = /&(?:amp|lt|gt|quot|apos|#39|#x27|#x2F);/g;
function stripHtmlTags(html) {
	return html.replace(/<[^>]*>/g, "").replace(ENTITY_RE, (m) => ENTITY_MAP[m] ?? m).trim();
}
function extractMediaFromHtml(html) {
	const urls = [];
	const srcRe = /<(?:img|video)[^>]+\bsrc\s*=\s*["']([^"']+)["']/gi;
	let match;
	while ((match = srcRe.exec(html)) !== null) if (match[1]) urls.push(match[1]);
	return urls;
}
function unwrapCdata(raw) {
	return raw.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1").trim();
}
function extractTag(xml, tag) {
	const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
	const m = xml.match(re);
	return m ? unwrapCdata(m[1]) : "";
}
function parseItems(xml, username, maxItems) {
	const items = [];
	const itemRe = /<item>([\s\S]*?)<\/item>/gi;
	let itemMatch;
	while ((itemMatch = itemRe.exec(xml)) !== null) {
		if (items.length >= maxItems) break;
		const block = itemMatch[1];
		const title = stripHtmlTags(extractTag(block, "title"));
		const link = stripHtmlTags(extractTag(block, "link"));
		const guid = stripHtmlTags(extractTag(block, "guid"));
		const rawDescription = extractTag(block, "description");
		const content = stripHtmlTags(rawDescription);
		const mediaUrls = extractMediaFromHtml(rawDescription);
		const pubDateRaw = stripHtmlTags(extractTag(block, "pubDate"));
		const pubDateMs = pubDateRaw ? new Date(pubDateRaw).getTime() : 0;
		const pubDate = pubDateMs ? new Date(pubDateMs).toISOString() : "";
		const isRetweet = title.startsWith("RT by") || content.startsWith("RT @");
		const isReply = content.startsWith("@") || link.includes("/status/") && link.includes("#m");
		items.push({
			id: guid || link,
			username,
			title,
			content,
			link,
			pubDate,
			pubDateMs,
			isRetweet,
			isReply,
			mediaUrls
		});
	}
	return items;
}
async function fetchNitterRss(opts) {
	const { instanceUrl, username, maxItems = 20, timeoutMs = 15e3 } = opts;
	const url = `${instanceUrl.replace(/\/+$/, "")}/${username.replace(/^@/, "")}/rss`;
	let res;
	try {
		res = await globalThis.fetch(url, {
			signal: AbortSignal.timeout(timeoutMs),
			headers: { Accept: "application/rss+xml, application/xml, text/xml" }
		});
	} catch (err) {
		return {
			ok: false,
			error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`
		};
	}
	if (!res.ok) return {
		ok: false,
		error: `HTTP ${res.status} ${res.statusText}`,
		statusCode: res.status
	};
	let xml;
	try {
		xml = await res.text();
	} catch (err) {
		return {
			ok: false,
			error: `Failed to read response body: ${err instanceof Error ? err.message : String(err)}`
		};
	}
	return {
		ok: true,
		items: parseItems(xml, username.replace(/^@/, ""), maxItems)
	};
}

//#endregion
//#region extensions/nitter/index.ts
function register(api) {
	const config = parseNitterConfig(api.pluginConfig);
	if (!config.enabled) return;
	api.registerTool(((_ctx) => createNitterTool({
		config,
		getFeedState: getActiveFeedState,
		fetchRss: fetchNitterRss
	})), { optional: true });
	api.registerService(createNitterPollerService({
		api: {
			enqueueSystemEvent: api.runtime.system.enqueueSystemEvent,
			logger: api.logger
		},
		config,
		fetchRss: fetchNitterRss,
		FeedStateManager,
		deliverItems: deliverFeedItems
	}));
	api.registerCommand({
		name: "nitter",
		description: "Show Nitter feed monitor status",
		acceptsArgs: false,
		handler: async () => {
			const feedState = getActiveFeedState();
			const subs = feedState?.listSubscriptions() ?? [];
			const enabledCount = subs.filter((s) => s.enabled).length;
			const lines = [
				"Nitter Feed Monitor",
				`Instance: ${config.instanceUrl}`,
				`Delivery mode: ${config.deliveryMode}`,
				`Poll interval: ${Math.round(config.pollIntervalMs / 1e3)}s`,
				`Active feeds: ${enabledCount}/${subs.length}`
			];
			if (subs.length > 0) {
				lines.push("", "Feeds:");
				for (const sub of subs) {
					const state = feedState?.getPollState(sub.username);
					const lastPoll = state?.lastPollAtMs ? `${Math.round((Date.now() - state.lastPollAtMs) / 1e3)}s ago` : "never";
					const status = sub.enabled ? "✓" : "✗";
					const label = sub.label ? ` (${sub.label})` : "";
					lines.push(`  ${status} @${sub.username}${label} \u2014 last poll: ${lastPoll}`);
				}
			}
			return { text: lines.join("\n") };
		}
	});
}

//#endregion
export { register as default };