"""
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"