"""
Formatting: escaping (esc_param, esc_path_info, esc_url, esc_path, sanitize),
quot_cec, quot_upr, unquote, untabify, to_utf8, age_class, age_string.
Ported from gitweb/gitweb.perl.
"""

from __future__ import annotations

import html
import re
from urllib.parse import quote, quote_plus

# Fallback encoding when bytes are not valid UTF-8 (gitweb: $fallback_encoding)
FALLBACK_ENCODING = "latin1"

# Control character escape codes (CEC). Port of quot_cec.
_CEC_MAP = {
	"\t": r"\t",
	"\n": r"\n",
	"\r": r"\r",
	"\f": r"\f",
	"\b": r"\b",
	"\a": r"\a",
	"\x1b": r"\e",
	"\v": r"\v",
	"\0": r"\0",
}


def to_utf8(s: str | bytes | None) -> str | None:
	"""Decode to UTF-8 string; use fallback encoding if not valid UTF-8. Port of to_utf8."""
	if s is None:
		return None
	if isinstance(s, str):
		return s
	try:
		return s.decode("utf-8")
	except UnicodeDecodeError:
		return s.decode(FALLBACK_ENCODING, errors="replace")


def esc_param(s: str | None) -> str | None:
	"""URL-encode for query param; keep / and space as +. Port of esc_param."""
	if s is None:
		return None
	return quote_plus(s, safe="")  # gitweb keeps -_.~()/@: and space→+


def esc_path_info(s: str | None) -> str | None:
	"""Path segment encoding; ? must be escaped. Port of esc_path_info."""
	if s is None:
		return None
	# Safe: A-Za-z0-9\-_.~();/;:@&= +
	return quote(s, safe="-_.~();/:@&= ")


def esc_url(s: str | None) -> str | None:
	"""URL encoding for href. Port of esc_url (same idea as esc_param)."""
	if s is None:
		return None
	return quote(s, safe="-_.~()/:@!")


def quot_cec(char: str, nohtml: bool = False) -> str:
	"""Printable representation of control char (CEC). Port of quot_cec."""
	out = _CEC_MAP.get(char, f"\\{ord(char):02x}")
	if nohtml:
		return out
	return f'<span class="cntrl">{out}</span>'


def quot_upr(char: str, nohtml: bool = False) -> str:
	"""Unicode control pictures. Port of quot_upr."""
	code = 0x2400 + ord(char)
	out = f"&#{code};"
	if nohtml:
		return out
	return f'<span class="cntrl">{out}</span>'


def esc_path(s: str | None, nbsp: bool = False) -> str | None:
	"""UTF-8, HTML-escape, then control chars to quot_cec. Port of esc_path."""
	if s is None:
		return None
	s = to_utf8(s) or s
	s = html.escape(s, quote=False)
	if nbsp:
		s = s.replace(" ", "&nbsp;")
	result = []
	for c in s:
		if ord(c) < 32 or ord(c) == 127:
			result.append(quot_cec(c))
		else:
			result.append(c)
	return "".join(result)


def sanitize(s: str | None) -> str | None:
	"""XHTML-safe: control chars to CEC except tab/lf/cr. Port of sanitize."""
	if s is None:
		return None
	s = to_utf8(s) or s
	result = []
	for c in s:
		if c in "\t\n\r":
			result.append(c)
		elif ord(c) < 32 or ord(c) == 127:
			result.append(quot_cec(c, nohtml=True))
		else:
			result.append(c)
	return "".join(result)


def unquote(s: str | None) -> str:
	"""Unescape git-style quoted filenames (C and octal). Port of unquote."""
	if s is None:
		return ""

	def unq(seq: str) -> str:
		es = {
			"t": "\t",
			"n": "\n",
			"r": "\r",
			"f": "\f",
			"b": "\b",
			"a": "\a",
			"e": "\x1b",
			"v": "\v",
		}
		if re.match(r"^[0-7]{1,3}$", seq):
			return chr(int(seq, 8))
		return es.get(seq, seq)

	if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
		s = s[1:-1]
		s = re.sub(r"\\([^0-7]|[0-7]{1,3})", lambda m: unq(m.group(1)), s)
	return s


def untabify(line: str, tabwidth: int = 8) -> str:
	"""Expand tabs to spaces. Port of untabify."""
	result = []
	col = 0
	for c in line:
		if c == "\t":
			n = tabwidth - (col % tabwidth)
			result.append(" " * n)
			col += n
		else:
			result.append(c)
			col += 1
	return "".join(result)


def age_class(age_seconds: float | None) -> str:
	"""CSS class for age. Port of age_class."""
	if age_seconds is None:
		return "noage"
	if age_seconds < 2 * 3600:
		return "age0"
	if age_seconds < 2 * 86400:
		return "age1"
	return "age2"


def age_string(age_seconds: float) -> str:
	"""Human-readable age. Port of age_string."""
	if age_seconds > 2 * 365 * 86400:
		return f"{int(age_seconds / 86400 / 365)} years ago"
	if age_seconds > 2 * (365 / 12) * 86400:
		return f"{int(age_seconds / 86400 / (365 / 12))} months ago"
	if age_seconds > 2 * 7 * 86400:
		return f"{int(age_seconds / 86400 / 7)} weeks ago"
	if age_seconds > 2 * 86400:
		return f"{int(age_seconds / 86400)} days ago"
	if age_seconds > 2 * 3600:
		return f"{int(age_seconds / 3600)} hours ago"
	if age_seconds > 2 * 60:
		return f"{int(age_seconds / 60)} min ago"
	if age_seconds > 2:
		return f"{int(age_seconds)} sec ago"
	return "right now"