"""
Formatting: escaping (esc_param, esc_path_info, esc_url, esc_attr, esc_html, esc_path, sanitize),
quot_cec, quot_upr, unquote, untabify, to_utf8, chop_str, chop_and_escape_str, age_class, age_string.
Ported from gitweb/gitweb.perl.
"""
from __future__ import annotations

import html
import re
from urllib.parse import quote, quote_plus, unquote as url_unquote

# 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 esc_attr(s: str | None) -> str | None:
    """Escape for HTML attribute. Port of esc_attr."""
    if s is None:
        return None
    return html.escape(s, quote=True)


def esc_html(s: str | None) -> str | None:
    """Escape for HTML body. Port of esc_html."""
    if s is None:
        return None
    return html.escape(s, quote=False)


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 chop_str(
    s: str,
    length: int,
    add_len: int = 10,
    where: str = "right",
) -> str:
    """Chop on word boundary between length and length+add_len. Port of chop_str."""
    s = to_utf8(s) or ""
    if where == "center":
        if length + 5 >= len(s):
            return s
        half = length // 2
        endre = re.compile(rf".{{{half}}}\w{{0,{add_len}}}", re.DOTALL)
        begre = re.compile(rf"\w{{0,{add_len}}}.{{{half}}}$", re.DOTALL)
        m1 = re.match(rf"^(.{{{half}}}\w{{0,{add_len}}})(.*)$", s, re.DOTALL)
        if not m1:
            return s[: length // 2] + " ... " + s[-(length // 2) :]
        left, rest = m1.group(1), m1.group(2)
        m2 = re.match(rf"^(.*?)(\w{{0,{add_len}}}.{{{half}}})$", rest, re.DOTALL)
        if not m2:
            return left + " ... " + rest[-half:]
        mid, right = m2.group(1), m2.group(2)
        if len(mid) > 5:
            mid = " ... "
        return left + mid + right
    if length + 4 >= len(s):
        return s
    if where == "left":
        begre = re.compile(rf"\w{{0,{add_len}}}.{{{length}}}$")
        m = begre.search(s)
        if m:
            body = m.group(0)
            lead = s[: m.start()]
            if len(lead) > 4:
                lead = " ..."
            return lead + body
        return s
    # right
    endre = re.compile(rf".{{{length}}}\w{{0,{add_len}}}")
    m = endre.match(s)
    if m:
        body = m.group(0)
        tail = s[m.end() :]
        if len(tail) > 4:
            tail = "... "
        return body + tail
    return s


def chop_and_escape_str(
    s: str,
    length: int,
    add_len: int = 10,
    where: str = "right",
) -> str:
    """Chop then HTML-escape; wrap in span with title if chopped. Port of chop_and_escape_str."""
    chopped = chop_str(s, length, add_len, where)
    s = to_utf8(s) or s
    if chopped == s:
        return esc_html(chopped) or ""
    title = esc_attr(s.replace("\n", " ").replace("\r", "?"))
    escaped = esc_html(chopped) or ""
    return f'<span title="{title}">{escaped}</span>'


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"