"""sshj.fuzzy — subsequence matching + query ranking (leaf).

B2 step 2: moved verbatim from sshj_cli.py (single-file era) — no
behavior change. Pure functions over host dicts; no intra-project
imports.
"""

def fuzzy(needle, hay):
    """Subsequence match: every char of needle appears in hay, in order."""
    it = iter(hay)
    return all(c in it for c in needle)


def sort_by_last_used(hs, last_used):
    """Newest last-called first (atuin-style); unrecorded keep file order."""
    return sorted(hs, key=lambda h: last_used.get(h["alias"], 0),
                  reverse=True)


def sort_by_last_success(hs, last_success, last_used=None):
    """Newest last SUCCESSFUL connection first (2026-09-21). A host never
    successfully connected falls back to its last-used time, then 0 — so a
    tried-but-failed host still surfaces in the order it was tried, while a
    host that genuinely succeeded last sorts above it."""
    last_used = last_used or {}
    return sorted(hs, key=lambda h: last_success.get(h["alias"])
                  or last_used.get(h["alias"], 0), reverse=True)


def sort_alpha(hs):
    """Case-insensitive alphabetical by the displayed name (disp)."""
    return sorted(hs, key=lambda h: h.get("disp", h["alias"]).lower())


def sort_hosts(hs, mode, last_used, last_success=None):
    """The mode-dispatching base order for the grid's pre-filter list.
    'last' (default) = last-called; 'last_success' = last successful
    connection; 'alpha' = alphabetical. Unknown modes fall back to 'last'."""
    if mode == "alpha":
        return sort_alpha(hs)
    if mode == "last_success":
        return sort_by_last_success(hs, last_success or {}, last_used)
    return sort_by_last_used(hs, last_used)


SORT_LABELS = {"last": "recent", "last_success": "last success",
               "alpha": "alpha"}


def fuzzy_score(needle, hay):
    """atuin-style subsequence score. 0 = no match; higher = better.

    Rewards prefix hits, contiguous runs, earlier matches and tight
    spans. Subsumes fuzzy() (a match is fuzzy_score(n,h) > 0).

    Whitespace in the QUERY is ignored (2026-09-21): the space-disables-
    keybindings rule means a leading space is common (" j" types a space
    then the j), and a space is a perfectly good separator when searching
    "user host". Matching on the non-space characters keeps those working.
    """
    if not needle:
        return 1
    nl = needle.lower()
    hl = hay.lower()
    pos = []
    i = 0
    for c in nl:
        if c == " ":
            continue
        i = hl.find(c, i)
        if i < 0:
            return 0
        pos.append(i)
        i += 1
    if not pos:                     # needle was spaces only -> matches all
        return 1
    score = 0.0
    for k, p in enumerate(pos):
        score += 10
        if k == 0 and p == 0:
            score += 30          # prefix hit
        if k > 0 and p == pos[k - 1] + 1:
            score += 6           # contiguous run
        score -= min(p, 40) * 0.1  # prefer earlier matches
    span = pos[-1] - pos[0] + 1
    score -= span * 0.2          # prefer tight matches
    return score


def best_field_score(h, q):
    """Score a host against q across name / username / address (B9).

    The name (alias) weighs most, then username, then address; a host
    whose query hits several fields gets a small additive on top of its
    best field. 0 = no field matches."""
    cands = [fuzzy_score(q, h.get("disp", "")) * 3.0]
    if h.get("user"):
        cands.append(fuzzy_score(q, h["user"]) * 2.0)
    if h.get("hostname"):
        cands.append(fuzzy_score(q, h["hostname"]) * 2.0)
    cands.append(fuzzy_score(q, h.get("target", "")) * 1.0)
    cands = [c for c in cands if c > 0]
    if not cands:
        return 0.0
    cands.sort(reverse=True)
    total = cands[0]
    for c in cands[1:]:
        total += c * 0.25
    return total


def query_rank(hosts, query):
    """(score, host) pairs matching `query`, best first.

    Stable on ties, so the caller's input order (last-used) is kept for
    equal scores. Empty if nothing matches."""
    scored = [(best_field_score(h, query), h) for h in hosts]
    scored = [(s, h) for s, h in scored if s > 0]
    scored.sort(key=lambda t: -t[0])
    return scored


def visible_hosts(hosts, query):
    """Hosts matching the live filter, ranked best-match first (B9).

    No query -> unchanged order (already last-used sorted). A query that
    matches nothing returns [] (the caller falls back to the full list)."""
    if not query:
        return hosts
    ranked = query_rank(hosts, query)
    return [h for _, h in ranked]


def direct_select(pool, q, last_used):
    """B8/B9 direct execution. q == "1" -> the first host (pool must be
    last-used sorted: row 1 of the grid = the last connection). Any other
    q -> best fuzzy match across name / username / address.

    Returns (chosen or None, all matching candidates). Empty candidates =
    no match (main() reports and exits 2)."""
    if q == "1":
        return (pool[0] if pool else None), list(pool)
    ranked = query_rank(pool, q)
    return (ranked[0][1] if ranked else None), [h for _, h in ranked]


