/** * Which language a code block is written in — as TELEGRAM understands it. * * Telegram highlights `
` (and the ```X fence, which
 * becomes the same thing) with libprisma, its own C++ port of Prism:
 * https://github.com/TelegramMessenger/libprisma. `X` is matched against Prism's
 * language ids and aliases, and against NOTHING else: an id it does not know is not a
 * fallback, it is no highlighting at all.
 *
 * That is why this file exists rather than a hopeful `path.split(".").pop()`. The
 * names are not the ones people habitually type:
 *
 * - Rust is `rust`. There is no `rs`.
 * - C++ is `cpp`. There is no `c++`.
 * - Go is `go`. There is no `golang`.
 * - But `py`, `ts`, `js`, `rb`, `yml`, `md`, `sh` ARE real aliases.
 *
 * So we do two things: map a FILE to its language (for the tool cards, which know the
 * path but have no fence to read), and normalise a language the MODEL wrote (`rs` →
 * `rust`), because an instruction is a request and this is a guarantee.
 */

/**
 * Every language id and alias libprisma accepts, verbatim from its README (the
 * "Supported Languages" table). Kept whole rather than pruned: it is the definition of
 * what Telegram will highlight, and a shorter list would just be a guess about what
 * someone might paste.
 */
export const TELEGRAM_CODE_LANGUAGES: ReadonlySet = new Set([
	// markup and friends
	"markup",
	"html",
	"xml",
	"svg",
	"mathml",
	"ssml",
	"atom",
	"rss",
	"css",
	"clike",
	"regex",
	"javascript",
	"js",
	// the long tail, alphabetically as libprisma lists it
	"abap",
	"abnf",
	"actionscript",
	"ada",
	"agda",
	"al",
	"antlr4",
	"g4",
	"apacheconf",
	"sql",
	"apex",
	"apl",
	"applescript",
	"aql",
	"c",
	"cpp",
	"arduino",
	"ino",
	"arff",
	"armasm",
	"arm-asm",
	"bash",
	"sh",
	"shell",
	"yaml",
	"yml",
	"markdown",
	"md",
	"arturo",
	"art",
	"asciidoc",
	"adoc",
	"csharp",
	"cs",
	"dotnet",
	"aspnet",
	"asm6502",
	"asmatmel",
	"autohotkey",
	"autoit",
	"avisynth",
	"avs",
	"avro-idl",
	"avdl",
	"awk",
	"gawk",
	"basic",
	"batch",
	"bbcode",
	"shortcode",
	"bbj",
	"bicep",
	"birb",
	"bison",
	"bnf",
	"rbnf",
	"bqn",
	"brainfuck",
	"brightscript",
	"bro",
	"cfscript",
	"cfc",
	"chaiscript",
	"cil",
	"cilkc",
	"cilk-c",
	"cilkcpp",
	"cilk-cpp",
	"cilk",
	"clojure",
	"cmake",
	"cobol",
	"coffeescript",
	"coffee",
	"concurnas",
	"conc",
	"csp",
	"cooklang",
	"ruby",
	"rb",
	"crystal",
	"csv",
	"cue",
	"cypher",
	"d",
	"dart",
	"dataweave",
	"dax",
	"dhall",
	"diff",
	"markup-templating",
	"django",
	"jinja2",
	"dns-zone-file",
	"dns-zone",
	"docker",
	"dockerfile",
	"dot",
	"gv",
	"ebnf",
	"editorconfig",
	"eiffel",
	"ejs",
	"eta",
	"elixir",
	"elm",
	"lua",
	"etlua",
	"erb",
	"erlang",
	"excel-formula",
	"xlsx",
	"xls",
	"fsharp",
	"factor",
	"false",
	"fift",
	"firestore-security-rules",
	"flow",
	"fortran",
	"ftl",
	"func",
	"gml",
	"gamemakerlanguage",
	"gap",
	"gcode",
	"gdscript",
	"gedcom",
	"gettext",
	"po",
	"git",
	"glsl",
	"gn",
	"gni",
	"linker-script",
	"ld",
	"go",
	"go-module",
	"go-mod",
	"gradle",
	"graphql",
	"groovy",
	"less",
	"scss",
	"textile",
	"haml",
	"handlebars",
	"hbs",
	"mustache",
	"haskell",
	"hs",
	"haxe",
	"hcl",
	"hlsl",
	"hoon",
	"hpkp",
	"hsts",
	"json",
	"webmanifest",
	"uri",
	"url",
	"http",
	"ichigojam",
	"icon",
	"icu-message-format",
	"idris",
	"idr",
	"ignore",
	"gitignore",
	"hgignore",
	"npmignore",
	"inform7",
	"ini",
	"io",
	"j",
	"java",
	"scala",
	"php",
	"javadoclike",
	"javadoc",
	"javastacktrace",
	"jolie",
	"jq",
	"typescript",
	"ts",
	"jsdoc",
	"n4js",
	"n4jsd",
	"json5",
	"jsonp",
	"jsstacktrace",
	"julia",
	"keepalived",
	"keyman",
	"kotlin",
	"kt",
	"kts",
	"kusto",
	"latex",
	"tex",
	"context",
	"latte",
	"scheme",
	"lilypond",
	"ly",
	"liquid",
	"lisp",
	"emacs",
	"elisp",
	"emacs-lisp",
	"livescript",
	"llvm",
	"log",
	"lolcode",
	"magma",
	"makefile",
	"mata",
	"matlab",
	"maxscript",
	"mel",
	"mermaid",
	"metafont",
	"mizar",
	"mongodb",
	"monkey",
	"moonscript",
	"moon",
	"n1ql",
	"nand2tetris-hdl",
	"naniscript",
	"nani",
	"nasm",
	"neon",
	"nevod",
	"nginx",
	"nim",
	"nix",
	"nsis",
	"objectivec",
	"objc",
	"ocaml",
	"odin",
	"opencl",
	"openqasm",
	"qasm",
	"oz",
	"parigp",
	"parser",
	"pascal",
	"objectpascal",
	"pascaligo",
	"psl",
	"pcaxis",
	"px",
	"peoplecode",
	"pcode",
	"perl",
	"phpdoc",
	"plant-uml",
	"plantuml",
	"plsql",
	"powerquery",
	"pq",
	"mscript",
	"powershell",
	"processing",
	"prolog",
	"promql",
	"properties",
	"protobuf",
	"stylus",
	"twig",
	"pug",
	"puppet",
	"purebasic",
	"pbfasm",
	"python",
	"py",
	"qsharp",
	"qs",
	"q",
	"qml",
	"qore",
	"r",
	"racket",
	"rkt",
	"cshtml",
	"razor",
	"jsx",
	"tsx",
	"reason",
	"rego",
	"renpy",
	"rpy",
	"rescript",
	"res",
	"rest",
	"rip",
	"roboconf",
	"robotframework",
	"robot",
	"rust",
	"sas",
	"sass",
	"shell-session",
	"sh-session",
	"shellsession",
	"smali",
	"smalltalk",
	"smarty",
	"sml",
	"smlnj",
	"solidity",
	"sol",
	"solution-file",
	"sln",
	"soy",
	"splunk-spl",
	"sqf",
	"squirrel",
	"stan",
	"stata",
	"iecst",
	"supercollider",
	"sclang",
	"swift",
	"systemd",
	"tact",
	"t4-templating",
	"t4-cs",
	"t4",
	"vbnet",
	"t4-vb",
	"tap",
	"tcl",
	"tt2",
	"toml",
	"tremor",
	"trickle",
	"troy",
	"tl",
	"tlb",
	"typoscript",
	"tsconfig",
	"unrealscript",
	"uscript",
	"uc",
	"uorazor",
	"v",
	"vala",
	"velocity",
	"verilog",
	"vhdl",
	"vim",
	"visual-basic",
	"vb",
	"vba",
	"warpscript",
	"wasm",
	"web-idl",
	"webidl",
	"wgsl",
	"wiki",
	"wolfram",
	"mathematica",
	"nb",
	"wl",
	"wren",
	"xeora",
	"xeoracube",
	"xojo",
	"xquery",
	"yang",
	"zig",
]);

/**
 * The names people (and models) reach for that libprisma does NOT know, and what they
 * plainly meant. Every value here is a real id above — this is a correction, never an
 * invention.
 */
const CORRECTIONS: Readonly> = {
	rs: "rust",
	golang: "go",
	"c++": "cpp",
	cplusplus: "cpp",
	cxx: "cpp",
	"c#": "csharp",
	"objective-c": "objectivec",
	python3: "python",
	py3: "python",
	node: "javascript",
	nodejs: "javascript",
	shellscript: "bash",
	zsh: "bash",
	console: "shell-session",
	terminal: "shell-session",
	htm: "html",
	yamlfile: "yaml",
	proto: "protobuf",
	patch: "diff",
	vue: "html",
	tf: "hcl",
	terraform: "hcl",
};

/** File extension → the language Telegram knows it by. */
const BY_EXTENSION: Readonly> = {
	ts: "typescript",
	mts: "typescript",
	cts: "typescript",
	tsx: "tsx",
	js: "javascript",
	mjs: "javascript",
	cjs: "javascript",
	jsx: "jsx",
	rs: "rust",
	py: "python",
	pyi: "python",
	rb: "ruby",
	go: "go",
	java: "java",
	kt: "kotlin",
	kts: "kotlin",
	swift: "swift",
	c: "c",
	h: "c",
	cpp: "cpp",
	cc: "cpp",
	cxx: "cpp",
	hpp: "cpp",
	hh: "cpp",
	cs: "csharp",
	php: "php",
	sh: "bash",
	bash: "bash",
	zsh: "bash",
	ps1: "powershell",
	sql: "sql",
	json: "json",
	jsonc: "json",
	json5: "json5",
	yaml: "yaml",
	yml: "yaml",
	toml: "toml",
	xml: "xml",
	svg: "svg",
	html: "html",
	htm: "html",
	css: "css",
	scss: "scss",
	sass: "sass",
	less: "less",
	md: "markdown",
	markdown: "markdown",
	lua: "lua",
	r: "r",
	jl: "julia",
	dart: "dart",
	ex: "elixir",
	exs: "elixir",
	erl: "erlang",
	hs: "haskell",
	scala: "scala",
	clj: "clojure",
	vim: "vim",
	zig: "zig",
	nim: "nim",
	sol: "solidity",
	tf: "hcl",
	hcl: "hcl",
	gradle: "gradle",
	groovy: "groovy",
	pl: "perl",
	pm: "perl",
	ml: "ocaml",
	fs: "fsharp",
	proto: "protobuf",
	graphql: "graphql",
	gql: "graphql",
	tex: "latex",
	diff: "diff",
	patch: "diff",
	bat: "batch",
	cmd: "batch",
	ini: "ini",
	cfg: "ini",
	cmake: "cmake",
	log: "log",
	csv: "csv",
};

/** Whole file names that name their own language (they have no useful extension). */
const BY_FILENAME: Readonly> = {
	dockerfile: "docker",
	makefile: "makefile",
	"cmakelists.txt": "cmake",
	".gitignore": "ignore",
	".npmignore": "ignore",
	".editorconfig": "editorconfig",
	"go.mod": "go-module",
	"go.sum": "go-module",
	".env": "ini",
};

/**
 * The language of the file at `path`, or undefined when we do not know it — in which
 * case the block goes out untagged, which is honest: a wrong tag highlights the code
 * as the wrong language, which is worse than plain text.
 */
export function languageForPath(path: string | undefined): string | undefined {
	if (!path) return undefined;
	const name = path.replace(/\\/g, "/").split("/").pop()?.trim().toLowerCase();
	if (!name) return undefined;
	const byName = BY_FILENAME[name];
	if (byName) return byName;
	// A dotfile (".gitignore") has no extension; anything else takes the last one.
	const dot = name.lastIndexOf(".");
	if (dot <= 0) return undefined;
	return BY_EXTENSION[name.slice(dot + 1)];
}

/**
 * The language a MODEL asked for, as Telegram will actually take it: a known id passes
 * through, a common mistake is corrected (`rs` → `rust`), and anything else returns
 * undefined so the fence is left untagged rather than tagged with a lie.
 */
export function normalizeLanguage(
	name: string | undefined,
): string | undefined {
	if (!name) return undefined;
	const key = name.trim().toLowerCase();
	if (!key) return undefined;
	if (TELEGRAM_CODE_LANGUAGES.has(key)) return key;
	return CORRECTIONS[key];
}

/** An opening code fence, capturing the info string the model wrote after the backticks. */
const FENCE = /^(\s{0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)[^\n]*$/;

/**
 * Rewrite the language tag on every opening fence in a Markdown text so Telegram can
 * actually highlight it: `rs` becomes `rust`, `c++` becomes `cpp`, and a tag nobody
 * knows is dropped rather than sent to be ignored.
 *
 * Only the fence LINE is touched — never a byte of the code inside it, and never a
 * fence that is closing one. The model is told to tag its code (see the instructions);
 * this is what makes the tag land even when it uses the name it is used to.
 */
export function normalizeCodeFences(markdown: string): string {
	const lines = markdown.split("\n");
	let openFence: string | null = null;
	for (let i = 0; i < lines.length; i++) {
		const line = lines[i];
		const match = FENCE.exec(line);
		if (!match) continue;
		const [, indent, fence, info] = match;
		if (openFence) {
			// Inside a block: only a fence of the same kind, at least as long, closes it.
			if (
				fence[0] === openFence[0] &&
				fence.length >= openFence.length &&
				!info
			) {
				openFence = null;
			}
			continue;
		}
		openFence = fence;
		if (!info) continue;
		const language = normalizeLanguage(info);
		lines[i] = `${indent}${fence}${language ?? ""}`;
	}
	return lines.join("\n");
}