"""Porter Stemmer for RBSP.

Pure-Python implementation of the Porter stemming algorithm (1980).
Reduces English words to their morphological stem for better recall
in information retrieval.
"""

from __future__ import annotations
from typing import List


def _is_consonant(word: str, i: int) -> bool:
    """Check if character at position i is a consonant."""
    ch = word[i]
    if ch in "aeiou":
        return False
    if ch == "y":
        return i == 0 or not _is_consonant(word, i - 1)
    return True


def _measure(word: str) -> int:
    """Calculate the 'measure' m of a word — number of VC sequences.

    [C](VC)^m[V] where C = consonant sequence, V = vowel sequence.
    """
    n = len(word)
    i = 0
    # Skip initial consonants
    while i < n and _is_consonant(word, i):
        i += 1
    if i >= n:
        return 0
    m = 0
    while True:
        # Skip vowels
        while i < n and not _is_consonant(word, i):
            i += 1
        if i >= n:
            return m
        m += 1
        # Skip consonants
        while i < n and _is_consonant(word, i):
            i += 1
        if i >= n:
            return m


def _has_vowel(word: str) -> bool:
    """Check if stem contains a vowel."""
    return any(not _is_consonant(word, i) for i in range(len(word)))


def _ends_double_consonant(word: str) -> bool:
    """Check if word ends with a double consonant."""
    return (
        len(word) >= 2
        and word[-1] == word[-2]
        and _is_consonant(word, len(word) - 1)
    )


def _cvc(word: str) -> bool:
    """Check if word ends with consonant-vowel-consonant where final C is not w, x, or y."""
    n = len(word)
    if n < 3:
        return False
    return (
        _is_consonant(word, n - 1)
        and not _is_consonant(word, n - 2)
        and _is_consonant(word, n - 3)
        and word[-1] not in "wxy"
    )


def stem(word: str) -> str:
    """Apply Porter stemming to a single word.

    Args:
        word: A lowercase English word.

    Returns:
        The stemmed form.
    """
    if len(word) <= 2:
        return word

    # Step 1a
    if word.endswith("sses"):
        word = word[:-2]
    elif word.endswith("ies"):
        word = word[:-2]
    elif not word.endswith("ss") and word.endswith("s"):
        word = word[:-1]

    # Step 1b
    flag = False
    if word.endswith("eed"):
        base = word[:-3]
        if _measure(base) > 0:
            word = word[:-1]
    elif word.endswith("ed"):
        base = word[:-2]
        if _has_vowel(base):
            word = base
            flag = True
    elif word.endswith("ing"):
        base = word[:-3]
        if _has_vowel(base):
            word = base
            flag = True

    if flag:
        if word.endswith("at") or word.endswith("bl") or word.endswith("iz"):
            word += "e"
        elif _ends_double_consonant(word) and word[-1] not in "lsz":
            word = word[:-1]
        elif _measure(word) == 1 and _cvc(word):
            word += "e"

    # Step 1c
    if word.endswith("y") and _has_vowel(word[:-1]):
        word = word[:-1] + "i"

    # Step 2
    step2_map = {
        "ational": "ate", "tional": "tion", "enci": "ence",
        "anci": "ance", "izer": "ize", "abli": "able",
        "alli": "al", "entli": "ent", "eli": "e",
        "ousli": "ous", "ization": "ize", "ation": "ate",
        "ator": "ate", "alism": "al", "iveness": "ive",
        "fulness": "ful", "ousness": "ous", "aliti": "al",
        "iviti": "ive", "biliti": "ble",
    }
    for suffix, replacement in step2_map.items():
        if word.endswith(suffix):
            base = word[: -len(suffix)]
            if _measure(base) > 0:
                word = base + replacement
            break

    # Step 3
    step3_map = {
        "icate": "ic", "ative": "", "alize": "al",
        "iciti": "ic", "ical": "ic", "ful": "",
        "ness": "",
    }
    for suffix, replacement in step3_map.items():
        if word.endswith(suffix):
            base = word[: -len(suffix)]
            if _measure(base) > 0:
                word = base + replacement
            break

    # Step 4
    step4_suffixes = [
        "al", "ance", "ence", "er", "ic", "able", "ible",
        "ant", "ement", "ment", "ent", "ion", "ou", "ism",
        "ate", "iti", "ous", "ive", "ize",
    ]
    for suffix in step4_suffixes:
        if word.endswith(suffix):
            base = word[: -len(suffix)]
            if suffix == "ion":
                if _measure(base) > 1 and len(base) > 0 and base[-1] in "st":
                    word = base
            elif _measure(base) > 1:
                word = base
            break

    # Step 5a
    if word.endswith("e"):
        base = word[:-1]
        m = _measure(base)
        if m > 1 or (m == 1 and not _cvc(base)):
            word = base

    # Step 5b
    if _measure(word) > 1 and _ends_double_consonant(word) and word[-1] == "l":
        word = word[:-1]

    return word


def stem_tokens(tokens: List[str]) -> List[str]:
    """Stem a list of tokens.

    Args:
        tokens: List of lowercase tokens.

    Returns:
        List of stemmed tokens (same length, same order).
    """
    return [stem(t) for t in tokens]
