// AGPL-3.0-only
//
// This is free software for the public good of a permacomputer
// hosted at permacomputer.com, an always-on computer by the people, for the
// people. One which is durable, easy to repair, and distributed like tap water
// for machine learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around
// four values:
//
//   TRUTH    - First principles, math & science, open source code
//   FREEDOM  - Voluntary partnerships, freedom from tyranny & corporate control
//   HARMONY  - Minimal waste, self-renewing systems with diverse connections
//   LOVE     - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by making machine learning
// accessible to everyone through a free, open, embeddable chat interface.
// Code is seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// Copyright 2025-2026 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com

// Content extraction and processing functionality

import { human, delta2dict } from "./ago.js";

// URI translation rules — map dynamic page patterns to raw/plain text equivalents.
// Each rule: { pattern: RegExp matching the full URI, translate: (match) => rawURI }
const URI_TRANSLATIONS = [
	{
		// GitLab CI job pages → raw log output
		// e.g. https://git.example.com/group/project/-/jobs/12345 → .../jobs/12345/raw
		pattern: /^(https?:\/\/[^/]+\/.*\/-\/jobs\/\d+)\/?$/,
		translate: (match) => `${match[1]}/raw`,
	},
];

// Try to fetch raw content via URI translation (same-origin, cookies included).
// Returns { raw: string } on success, { matched: true } if pattern matched but
// fetch failed (signals a dynamic page worth waiting for), or null if no match.
async function fetchTranslatedContent(uri) {
	for (const rule of URI_TRANSLATIONS) {
		const match = uri.match(rule.pattern);
		if (match) {
			try {
				const rawUri = rule.translate(match);
				const response = await fetch(rawUri, { credentials: "same-origin" });
				if (response.ok) {
					const text = await response.text();
					if (text && text.length > 0) {
						return { raw: text };
					}
				}
			} catch (e) {
				console.warn("URI translation fetch failed, falling back to DOM:", e);
			}
			// Pattern matched but fetch failed — caller should wait for DOM to settle
			return { matched: true };
		}
	}
	return null;
}

// Wait for DOM mutations to settle (content stops streaming in)
function waitForDOMSettle(timeout = 3000, quiet = 500) {
	return new Promise((resolve) => {
		let timer;
		const observer = new MutationObserver(() => {
			clearTimeout(timer);
			timer = setTimeout(() => {
				observer.disconnect();
				resolve();
			}, quiet);
		});
		observer.observe(document.body, {
			childList: true,
			subtree: true,
			characterData: true,
		});
		// Start the quiet timer immediately in case DOM is already settled
		timer = setTimeout(() => {
			observer.disconnect();
			resolve();
		}, quiet);
		// Hard ceiling so we don't wait forever
		setTimeout(() => {
			observer.disconnect();
			resolve();
		}, timeout);
	});
}

// Extract DOM content — title, metadata, text with links
function extractDOMContent() {
	let content = "";

	// Extract title
	const title = document.title;
	if (title) {
		content += `**Page Title**: ${title}\n\n`;
	}

	// Extract meta description
	const metaDescription = document.querySelector('meta[name="description"]');
	if (metaDescription) {
		content += `**Meta Description**: ${metaDescription.content}\n\n`;
	}

	// Extract other metadata (if needed)
	const metaKeywords = document.querySelector('meta[name="keywords"]');
	if (metaKeywords) {
		content += `**Meta Keywords**: ${metaKeywords.content}\n\n`;
	}

	// Recursively extract text and links from the body content
	function getTextWithLinks(element) {
		if (element.nodeType === Node.TEXT_NODE) {
			content += `${element.textContent} `;
		} else if (element.nodeType === Node.ELEMENT_NODE) {
			if (element.tagName.toLowerCase() === "a") {
				// If it's a link, append the text and the href
				content += `[${element.textContent}](${element.href}) `;
			} else {
				// Recursively process child nodes
				element.childNodes.forEach(getTextWithLinks);
			}
		}
	}

	getTextWithLinks(document.body);
	return content.trim();
}

// Extract text content along with links and metadata from the webpage.
// Tries URI translation first (raw/plain text for known dynamic pages),
// falls back to DOM extraction.
export async function extractWebpageContent() {
	const result = await fetchTranslatedContent(window.location.href);

	// Raw content fetched successfully — use it directly
	if (result?.raw) {
		let content = "";
		const title = document.title;
		if (title) {
			content += `**Page Title**: ${title}\n\n`;
		}
		content += `**Source URI**: ${window.location.href}\n\n`;
		content += result.raw;
		return content.trim();
	}

	// Pattern matched but raw fetch failed (auth, CORS, etc) —
	// this is a known dynamic page, wait for DOM to settle before scraping
	if (result?.matched) {
		await waitForDOMSettle();
	}

	return extractDOMContent();
}

// --- Page Analysis (pre-inference classification) ---
// Runs before the greeting to extract structured intelligence about the page.
// The greeting uses these keywords instead of guessing from raw text.

// Build the classification prompt for a page
export function buildPageAnalysisPrompt(pageContent, pageTitle, pageUrl) {
	const currentDate = new Date().toISOString().split("T")[0];
	// Trim content to keep classification fast
	const contentPreview = pageContent.substring(0, 4000);

	return {
		system:
			"You are a page classification engine. Analyze web pages and extract structured metadata. Respond with ONLY a valid JSON object. No markdown fences, no explanation, no commentary. Just the JSON object.",
		user: `Classify this webpage. Current date: ${currentDate}

TITLE: ${pageTitle}
URI: ${pageUrl}

CONTENT:
${contentPreview}

Respond with this exact JSON structure (use null for unknown fields):
{
  "type": "<lyrics|recipe|blog|news|wiki|email|documentation|product|landing|portfolio|social|academic|code|tutorial|legal|ecommerce|video|podcast|event|job|review|qa|forum|gallery|ci-log|error-page|manifesto|personal|other>",
  "author": "<author name or null>",
  "publishDate": "<YYYY-MM-DD or null>",
  "topics": ["<primary>", "<secondary>"],
  "entities": ["<key names, orgs, technologies>"],
  "tone": "<technical|casual|formal|creative|academic|promotional|instructional|journalistic|poetic|humorous|philosophical>",
  "domain": "<technology|science|cooking|music|art|politics|business|health|education|entertainment|sports|finance|law|philosophy|nature|travel|other>",
  "audience": "<one phrase>",
  "keyPhrases": ["<3-5 notable verbatim phrases from the content>"],
  "summary": "<one sentence>",
  "contentLanguage": "<ISO 639-1>",
  "freshness": "<evergreen|dated|breaking|historical|timeless>"
}`,
	};
}

// Parse the JSON classification from LLM response
export function parsePageAnalysis(response) {
	try {
		const jsonMatch = response.match(/\{[\s\S]*\}/);
		if (jsonMatch) {
			return JSON.parse(jsonMatch[0]);
		}
	} catch (e) {
		console.warn("Page analysis parsing failed:", e);
	}
	return null;
}

// Build enriched context string from analysis for the greeting prompt
export function buildAnalysisContext(analysis) {
	if (!analysis) return "";

	const currentDate = new Date().toISOString().split("T")[0];
	const lines = [`\nPAGE INTELLIGENCE (pre-analyzed):`, `Current Date: ${currentDate}`, `Page Type: ${analysis.type}`];

	if (analysis.author) lines.push(`Author: ${analysis.author}`);
	if (analysis.publishDate) {
		lines.push(`Published: ${analysis.publishDate}`);
		// Use russell ballestrini's ago algorithm for human-readable time distance
		try {
			const publishedDate = new Date(analysis.publishDate);
			if (!Number.isNaN(publishedDate.getTime())) {
				const age = human(publishedDate, 2);
				lines.push(`Age: ${age}`);
			}
		} catch (e) {
			// LLM returned unparseable date, skip age
		}
	}

	if (analysis.topics?.length) lines.push(`Topics: ${analysis.topics.join(", ")}`);
	if (analysis.entities?.length) lines.push(`Key Entities: ${analysis.entities.join(", ")}`);
	if (analysis.tone) lines.push(`Tone: ${analysis.tone}`);
	if (analysis.domain) lines.push(`Domain: ${analysis.domain}`);
	if (analysis.audience) lines.push(`Audience: ${analysis.audience}`);
	if (analysis.keyPhrases?.length) lines.push(`Notable Phrases: "${analysis.keyPhrases.join('", "')}"`);
	if (analysis.summary) lines.push(`Summary: ${analysis.summary}`);
	if (analysis.contentLanguage) lines.push(`Content Language: ${analysis.contentLanguage}`);
	if (analysis.freshness) lines.push(`Freshness: ${analysis.freshness}`);

	return `${lines.join("\n")}\n`;
}
