// page-intelligence.js: Computed page metrics for augmenting LLM context
//
// Pure JS computation, zero API calls, instant execution.
// Each analyzer extracts deterministic ground truth from the DOM or page content.
// The LLM gets precise measurements instead of guessing from raw text.
//
// Copyright (C) 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// --- STRUCTURED DATA EXTRACTOR ---
// Open Graph, JSON-LD, Twitter Cards, canonical URI, author, publish time
export function extractStructuredData() {
	const result = {
		openGraph: {},
		jsonLd: [],
		twitterCards: {},
		canonical: null,
		author: null,
		publishedTime: null,
	};

	// Open Graph: meta[property^="og:"]
	for (const meta of document.querySelectorAll('meta[property^="og:"]')) {
		const key = meta.getAttribute("property").replace("og:", "");
		result.openGraph[key] = meta.getAttribute("content");
	}

	// JSON-LD: script[type="application/ld+json"]
	for (const script of document.querySelectorAll('script[type="application/ld+json"]')) {
		try {
			result.jsonLd.push(JSON.parse(script.textContent));
		} catch (e) {
			// Malformed JSON-LD, skip
		}
	}

	// Twitter Cards: meta[name^="twitter:"]
	for (const meta of document.querySelectorAll('meta[name^="twitter:"]')) {
		const key = meta.getAttribute("name").replace("twitter:", "");
		result.twitterCards[key] = meta.getAttribute("content");
	}

	// Canonical URI
	const canonical = document.querySelector('link[rel="canonical"]');
	if (canonical) result.canonical = canonical.getAttribute("href");

	// Author meta
	const authorMeta = document.querySelector('meta[name="author"]');
	if (authorMeta) result.author = authorMeta.getAttribute("content");

	// Article published time (Open Graph article extension)
	const publishedTime = document.querySelector('meta[property="article:published_time"]');
	if (publishedTime) result.publishedTime = publishedTime.getAttribute("content");

	return result;
}

// --- HEADING OUTLINE ---
// h1-h6 hierarchy as compact table of contents
export function extractHeadingOutline() {
	const headings = [];
	for (const el of document.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
		const level = Number.parseInt(el.tagName.charAt(1), 10);
		const text = el.textContent.trim();
		if (text) {
			headings.push({ level, text: text.substring(0, 100) });
		}
	}
	return headings;
}

// --- READING METRICS ---
// Word count, sentence count, paragraph count, reading time at 238 wpm (Brysbaert 2019)
export function computeReadingMetrics(pageContent) {
	if (!pageContent) return { wordCount: 0, sentenceCount: 0, paragraphCount: 0, readingTimeMinutes: 0 };

	// Strip markdown link syntax and metadata labels for cleaner count
	const cleanText = pageContent.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*[^*]+\*\*:\s*/g, "");

	const words = cleanText.match(/\b\w+\b/g) || [];
	const wordCount = words.length;
	const sentences = cleanText.match(/[^.!?]+[.!?]+/g) || [];
	const sentenceCount = Math.max(sentences.length, 1);
	const paragraphs = cleanText.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
	const paragraphCount = paragraphs.length;
	const readingTimeMinutes = Math.ceil(wordCount / 238);

	return { wordCount, sentenceCount, paragraphCount, readingTimeMinutes };
}

// --- SYLLABLE COUNTER ---
// Vowel-group heuristic for English words (~85% accuracy)
function countSyllables(word) {
	const w = word.toLowerCase().replace(/[^a-z]/g, "");
	if (w.length <= 2) return 1;

	let count = 0;
	const vowels = "aeiouy";
	let prevVowel = false;

	for (let i = 0; i < w.length; i++) {
		const isVowel = vowels.includes(w[i]);
		if (isVowel && !prevVowel) count++;
		prevVowel = isVowel;
	}

	// Silent trailing "e" (but not "le" as in "table")
	if (w.endsWith("e") && !w.endsWith("le") && count > 1) count--;

	return Math.max(count, 1);
}

// --- READABILITY SCORE ---
// Flesch-Kincaid Grade Level: 0.39 * (words/sentences) + 11.8 * (syllables/words) - 15.59
export function computeReadabilityScore(pageContent) {
	if (!pageContent) return { fleschKincaidGrade: 0, syllableCount: 0, wordsPerSentence: 0, syllablesPerWord: 0 };

	const cleanText = pageContent.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*[^*]+\*\*:\s*/g, "");

	const words = cleanText.match(/\b[a-zA-Z]+\b/g) || [];
	const wordCount = words.length;
	if (wordCount === 0) return { fleschKincaidGrade: 0, syllableCount: 0, wordsPerSentence: 0, syllablesPerWord: 0 };

	const sentences = cleanText.match(/[^.!?]+[.!?]+/g) || [];
	const sentenceCount = Math.max(sentences.length, 1);

	let syllableCount = 0;
	for (const word of words) {
		syllableCount += countSyllables(word);
	}

	const wordsPerSentence = wordCount / sentenceCount;
	const syllablesPerWord = syllableCount / wordCount;
	const grade = 0.39 * wordsPerSentence + 11.8 * syllablesPerWord - 15.59;

	return {
		fleschKincaidGrade: Math.round(grade * 10) / 10,
		syllableCount,
		wordsPerSentence: Math.round(wordsPerSentence * 10) / 10,
		syllablesPerWord: Math.round(syllablesPerWord * 100) / 100,
	};
}

// --- CODE DETECTION ---
// Code blocks, language detection from class patterns, code-to-prose ratio
export function detectCodeBlocks() {
	const result = {
		codeBlockCount: 0,
		languages: [],
		codeLineCount: 0,
		codeToProseRatio: 0,
		inlineCodeCount: 0,
	};

	const languageSet = new Set();
	const preCodeBlocks = document.querySelectorAll("pre code");
	result.codeBlockCount = preCodeBlocks.length;

	for (const block of preCodeBlocks) {
		result.codeLineCount += (block.textContent.match(/\n/g) || []).length + 1;

		for (const cls of block.classList) {
			const langMatch = cls.match(/^(?:language-|lang-)(.+)$/);
			if (langMatch && langMatch[1] !== "none" && langMatch[1] !== "plaintext") {
				languageSet.add(langMatch[1]);
			}
		}
	}

	// Inline code: code elements not inside pre
	const allCode = document.querySelectorAll("code");
	result.inlineCodeCount = Array.from(allCode).filter((el) => !el.parentElement || el.parentElement.tagName !== "PRE").length;

	result.languages = Array.from(languageSet);

	// Code-to-prose ratio by character count
	const bodyText = document.body?.textContent || "";
	const codeText = Array.from(preCodeBlocks)
		.map((b) => b.textContent)
		.join("");
	if (bodyText.length > 0) {
		result.codeToProseRatio = Math.round((codeText.length / bodyText.length) * 100) / 100;
	}

	return result;
}

// --- LINK TOPOLOGY ---
// Internal vs external links, top 5 external domains by frequency
export function analyzeLinkTopology() {
	const result = {
		internalCount: 0,
		externalCount: 0,
		totalCount: 0,
		topExternalDomains: [],
	};

	const currentHost = window.location.hostname;
	const domainCounts = {};

	for (const link of document.querySelectorAll("a[href]")) {
		result.totalCount++;
		try {
			const uri = new URL(link.href, window.location.origin);
			if (uri.hostname === currentHost || uri.hostname === "") {
				result.internalCount++;
			} else {
				result.externalCount++;
				domainCounts[uri.hostname] = (domainCounts[uri.hostname] || 0) + 1;
			}
		} catch (e) {
			result.internalCount++;
		}
	}

	result.topExternalDomains = Object.entries(domainCounts)
		.sort((a, b) => b[1] - a[1])
		.slice(0, 5)
		.map(([domain, count]) => ({ domain, count }));

	return result;
}

// --- USER CONTEXT ---
// Timezone, locale, local time, referrer, device type
export function captureUserContext() {
	const width = window.innerWidth;
	let deviceType = "desktop";
	if (width < 768) deviceType = "mobile";
	else if (width < 1024) deviceType = "tablet";

	return {
		timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown",
		language: navigator.language || "unknown",
		localTime: new Date().toLocaleTimeString("en-US", { hour12: false }),
		referrer: document.referrer || null,
		deviceType,
	};
}

// --- MEDIA INVENTORY ---
// Image count, alt text coverage, video embeds (youtube/vimeo), audio elements
export function inventoryMedia() {
	const images = document.querySelectorAll("img");
	const imagesWithAlt = Array.from(images).filter((img) => img.alt && img.alt.trim().length > 0).length;

	const videoEmbeds = [];
	for (const iframe of document.querySelectorAll("iframe")) {
		const src = iframe.src || "";
		if (src.includes("youtube.com") || src.includes("youtu.be")) {
			videoEmbeds.push({ type: "youtube", src });
		} else if (src.includes("vimeo.com")) {
			videoEmbeds.push({ type: "vimeo", src });
		}
	}

	return {
		imageCount: images.length,
		imagesWithAlt,
		altCoverage: images.length > 0 ? Math.round((imagesWithAlt / images.length) * 100) : 0,
		videoEmbeds,
		videoElements: document.querySelectorAll("video").length,
		audioElements: document.querySelectorAll("audio").length,
	};
}

// --- FORM DETECTION ---
// Classify forms: login, search, contact, upload, checkout, generic
export function detectForms() {
	return Array.from(document.querySelectorAll("form"))
		.slice(0, 10)
		.map((form) => {
			const inputs = Array.from(form.querySelectorAll("input, textarea, select"));
			const inputTypes = inputs.map((inp) => inp.type || inp.tagName.toLowerCase());

			let classification = "generic";
			if (inputTypes.includes("password")) {
				classification = "login";
			} else if (inputTypes.includes("search") || form.getAttribute("role") === "search") {
				classification = "search";
			} else if (inputTypes.includes("file")) {
				classification = "upload";
			} else if (inputTypes.includes("email") && inputTypes.includes("textarea")) {
				classification = "contact";
			} else if (
				inputTypes.some((t) => t === "tel") &&
				(form.innerHTML.toLowerCase().includes("card") ||
					form.innerHTML.toLowerCase().includes("payment") ||
					form.innerHTML.toLowerCase().includes("cvv"))
			) {
				classification = "checkout";
			}

			return { classification, fieldCount: inputs.length };
		});
}

// --- TABLE EXTRACTION ---
// Headers + row counts for up to 5 tables
export function extractTables() {
	return Array.from(document.querySelectorAll("table"))
		.slice(0, 5)
		.map((table) => {
			const headerRow = table.querySelector("thead tr") || table.querySelector("tr");
			const headers = headerRow
				? Array.from(headerRow.querySelectorAll("th")).map((cell) => cell.textContent.trim().substring(0, 50))
				: [];
			const bodyRows = table.querySelectorAll("tbody tr").length;
			const totalRows = table.querySelectorAll("tr").length;
			const rowCount = bodyRows > 0 ? bodyRows : Math.max(totalRows - (headers.length > 0 ? 1 : 0), 0);

			return { headers, rowCount };
		});
}

// --- ENTITY PATTERNS ---
// Regex extraction: emails, prices, dates, versions, IPs, percentages
export function extractEntityPatterns(pageContent) {
	if (!pageContent) return {};

	const dedupe = (matches) => [...new Set(matches || [])].slice(0, 10);

	const result = {
		emails: dedupe(pageContent.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g)),
		prices: dedupe(pageContent.match(/\$\d+(?:,\d{3})*(?:\.\d{2})?/g)),
		dates: dedupe(pageContent.match(/\d{4}-\d{2}-\d{2}/g)),
		versions: dedupe(pageContent.match(/v\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?/g)),
		ipAddresses: dedupe(pageContent.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g)),
		percentages: dedupe(pageContent.match(/\d+(?:\.\d+)?%/g)),
	};

	// Only return non-empty arrays
	return Object.fromEntries(Object.entries(result).filter(([_, v]) => v.length > 0));
}

// --- ORCHESTRATOR ---
// Runs all analyzers with try/catch per analyzer. One failure does not block others.
export function computePageIntelligence(pageContent) {
	const intelligence = {};

	// DOM-based analyzers (read from document, no arguments)
	const domAnalyzers = {
		structuredData: extractStructuredData,
		headingOutline: extractHeadingOutline,
		codeBlocks: detectCodeBlocks,
		linkTopology: analyzeLinkTopology,
		userContext: captureUserContext,
		media: inventoryMedia,
		forms: detectForms,
		tables: extractTables,
	};

	// Text-based analyzers (receive pageContent string)
	const textAnalyzers = {
		readingMetrics: computeReadingMetrics,
		readability: computeReadabilityScore,
		entityPatterns: extractEntityPatterns,
	};

	for (const [key, fn] of Object.entries(domAnalyzers)) {
		try {
			intelligence[key] = fn();
		} catch (e) {
			console.warn(`Page intelligence: ${key} failed:`, e);
			intelligence[key] = null;
		}
	}

	for (const [key, fn] of Object.entries(textAnalyzers)) {
		try {
			intelligence[key] = fn(pageContent);
		} catch (e) {
			console.warn(`Page intelligence: ${key} failed:`, e);
			intelligence[key] = null;
		}
	}

	return intelligence;
}

// Grade level description for readability scores
function describeGradeLevel(grade) {
	if (grade < 1) return "kindergarten";
	if (grade <= 5) return "elementary";
	if (grade <= 8) return `${Math.round(grade)}th grade`;
	if (grade <= 12) return "high school";
	if (grade <= 16) return "college level";
	return "graduate level";
}

// --- FORMATTER ---
// Token-efficient: only includes non-empty sections.
// Format: "Label: value | value | value" per line.
export function formatPageIntelligence(intelligence) {
	if (!intelligence) return "";

	const lines = [];

	// Reading Metrics
	const rm = intelligence.readingMetrics;
	if (rm && rm.wordCount > 0) {
		lines.push(`Reading: ${rm.wordCount.toLocaleString()} words | ${rm.sentenceCount} sentences | ${rm.paragraphCount} paragraphs | ~${rm.readingTimeMinutes} min read`);
	}

	// Readability
	const rs = intelligence.readability;
	if (rs && rs.fleschKincaidGrade > 0) {
		lines.push(`Readability: Flesch-Kincaid grade ${rs.fleschKincaidGrade} (${describeGradeLevel(rs.fleschKincaidGrade)})`);
	}

	// Heading Outline (compact, max 8)
	const ho = intelligence.headingOutline;
	if (ho && ho.length > 0) {
		const display = ho
			.slice(0, 8)
			.map((h) => `${"  ".repeat(h.level - 1)}h${h.level}: ${h.text}`)
			.join("\n  ");
		const suffix = ho.length > 8 ? `\n  +${ho.length - 8} more` : "";
		lines.push(`Headings:\n  ${display}${suffix}`);
	}

	// Code Detection
	const cd = intelligence.codeBlocks;
	if (cd && cd.codeBlockCount > 0) {
		const langStr = cd.languages.length > 0 ? ` (${cd.languages.join(", ")})` : "";
		const ratio = Math.round(cd.codeToProseRatio * 100);
		lines.push(`Code: ${cd.codeBlockCount} blocks${langStr} | ${cd.codeLineCount} lines | ${ratio}% code-to-prose`);
	}

	// Link Topology
	const lt = intelligence.linkTopology;
	if (lt && lt.totalCount > 0) {
		let linkLine = `Links: ${lt.internalCount} internal, ${lt.externalCount} external`;
		if (lt.topExternalDomains.length > 0) {
			const domains = lt.topExternalDomains.map((d) => `${d.domain}(${d.count})`).join(", ");
			linkLine += ` | top: ${domains}`;
		}
		lines.push(linkLine);
	}

	// Media Inventory
	const mi = intelligence.media;
	if (mi && (mi.imageCount > 0 || mi.videoEmbeds.length > 0 || mi.audioElements > 0)) {
		const parts = [];
		if (mi.imageCount > 0) parts.push(`${mi.imageCount} images (${mi.altCoverage}% with alt text)`);
		if (mi.videoEmbeds.length > 0) {
			const types = [...new Set(mi.videoEmbeds.map((v) => v.type))].join(", ");
			parts.push(`${mi.videoEmbeds.length} ${types} embed${mi.videoEmbeds.length > 1 ? "s" : ""}`);
		}
		if (mi.videoElements > 0) parts.push(`${mi.videoElements} video element${mi.videoElements > 1 ? "s" : ""}`);
		if (mi.audioElements > 0) parts.push(`${mi.audioElements} audio element${mi.audioElements > 1 ? "s" : ""}`);
		lines.push(`Media: ${parts.join(" | ")}`);
	}

	// Structured Data
	const sd = intelligence.structuredData;
	if (sd) {
		const parts = [];
		if (sd.openGraph.type) parts.push(`og:type=${sd.openGraph.type}`);
		if (sd.author) parts.push(`author=${sd.author}`);
		if (sd.publishedTime) parts.push(`published=${sd.publishedTime.split("T")[0]}`);
		if (sd.canonical) parts.push(`canonical=${sd.canonical}`);
		if (sd.jsonLd.length > 0) {
			const types = sd.jsonLd.map((j) => j["@type"]).filter(Boolean);
			if (types.length > 0) parts.push(`json-ld: ${types.join(", ")}`);
		}
		if (parts.length > 0) lines.push(`Structured Data: ${parts.join(" | ")}`);
	}

	// Entity Patterns
	const ep = intelligence.entityPatterns;
	if (ep && Object.keys(ep).length > 0) {
		const parts = [];
		if (ep.prices?.length > 0) parts.push(`prices: ${ep.prices.join(", ")}`);
		if (ep.versions?.length > 0) parts.push(`versions: ${ep.versions.join(", ")}`);
		if (ep.dates?.length > 0) parts.push(`dates: ${ep.dates.join(", ")}`);
		if (ep.emails?.length > 0) parts.push(`emails: ${ep.emails.length} found`);
		if (ep.ipAddresses?.length > 0) parts.push(`IPs: ${ep.ipAddresses.join(", ")}`);
		if (ep.percentages?.length > 0) parts.push(`percentages: ${ep.percentages.join(", ")}`);
		if (parts.length > 0) lines.push(`Entities: ${parts.join(" | ")}`);
	}

	// Forms
	const forms = intelligence.forms;
	if (forms && forms.length > 0) {
		const formTypes = forms.map((f) => f.classification).join(", ");
		lines.push(`Forms: ${forms.length} form${forms.length > 1 ? "s" : ""} (${formTypes})`);
	}

	// Tables
	const tables = intelligence.tables;
	if (tables && tables.length > 0) {
		const tableDescs = tables.map((t) => {
			const headerStr = t.headers.length > 0 ? t.headers.join(", ") : "no headers";
			return `(${headerStr} | ${t.rowCount} rows)`;
		});
		lines.push(`Tables: ${tables.length} table${tables.length > 1 ? "s" : ""} ${tableDescs.join(", ")}`);
	}

	// User Context
	const uc = intelligence.userContext;
	if (uc) {
		const parts = [uc.timezone, uc.language, uc.deviceType];
		if (uc.referrer) {
			try {
				parts.push(`from: ${new URL(uc.referrer).hostname}`);
			} catch (e) {
				parts.push(`from: ${uc.referrer}`);
			}
		}
		lines.push(`User: ${parts.join(" | ")}`);
	}

	if (lines.length === 0) return "";

	return `\nCOMPUTED PAGE METRICS (deterministic):\n${lines.join("\n")}\n`;
}
