"""Password generation utilities tailored for the Encryption Vault app.

This module focuses on producing passwords that strike a balance between
security and day-to-day usability.  Inspired by modern password managers,
it offers three families of generators:

* ``apple`` – hyphen separated segments with mixed case letters and digits,
  tuned to avoid visually ambiguous glyphs so that typing on mobile or
  desktop keyboards is less error-prone.
* ``memorable`` – short passphrases built from a curated word list with an
  optional numeric suffix, prioritising recall while keeping high entropy.
* ``random`` – legacy behaviour using the full printable alphabet for
  situations where maximum entropy takes precedence over readability.

The public ``generate_password`` helper routes to the appropriate strategy
while providing sensible defaults that mimic Apple's suggested password
formatting out of the box.
"""
from __future__ import annotations

import secrets
import string
from typing import Iterable, Sequence

# Character pools intentionally drop ambiguous glyphs such as 0/O and 1/l/I.
_UNAMBIGUOUS_DIGITS = "23456789"
_LOWER = "abcdefghjkmnpqrstuvwxyz"
_UPPER = "ABCDEFGHJKLMNPQRSTUVWXYZ"
_APPLE_CHARSET = _UNAMBIGUOUS_DIGITS + _LOWER + _UPPER

_DEFAULT_SEGMENT_LENGTH = 4
_MIN_SEGMENTS = 2
_MAX_SEGMENTS = 6

_DEFAULT_WORDS = 4
_MAX_WORDS = 8

_DEFAULT_SUFFIX_DIGITS = 2
_DEFAULT_SEPARATOR = "-"

# A compact, high-quality word list sourced from short, distinct English
# words.  Duplicates are avoided to preserve consistent entropy per draw.
_MEMORABLE_WORDS: Sequence[str] = (
    "acorn",
    "adapt",
    "amber",
    "angle",
    "apple",
    "april",
    "arbor",
    "arrow",
    "atlas",
    "aurora",
    "avocado",
    "badge",
    "balmy",
    "bamboo",
    "basil",
    "beacon",
    "berry",
    "birch",
    "bison",
    "bliss",
    "bloom",
    "breeze",
    "brick",
    "brisk",
    "brook",
    "cable",
    "cacao",
    "caper",
    "cedar",
    "chalk",
    "charm",
    "cider",
    "civet",
    "cobalt",
    "comet",
    "copper",
    "coral",
    "crown",
    "crisp",
    "daisy",
    "delta",
    "dingo",
    "dough",
    "dove",
    "ember",
    "fjord",
    "flair",
    "flora",
    "flute",
    "foggy",
    "forge",
    "frost",
    "fudge",
    "gemma",
    "gleam",
    "glint",
    "gnome",
    "grain",
    "grove",
    "guava",
    "harbor",
    "hazel",
    "honey",
    "icicle",
    "jade",
    "jolly",
    "kelpy",
    "keystone",
    "koala",
    "lagoon",
    "lantern",
    "lemon",
    "lilac",
    "lunar",
    "lyric",
    "magma",
    "maple",
    "meadow",
    "merit",
    "mirth",
    "mossy",
    "nimbus",
    "olive",
    "opal",
    "orbit",
    "otter",
    "palm",
    "papaya",
    "pearl",
    "pepper",
    "petal",
    "pine",
    "piper",
    "pixel",
    "plume",
    "prairie",
    "quartz",
    "quiet",
    "raven",
    "river",
    "saffron",
    "salmon",
    "sage",
    "satin",
    "savvy",
    "shade",
    "shiny",
    "silk",
    "siren",
    "skiff",
    "slate",
    "smoky",
    "spark",
    "spice",
    "sprout",
    "stone",
    "sugar",
    "swift",
    "tango",
    "taper",
    "terra",
    "thrive",
    "tidal",
    "topaz",
    "tulip",
    "valor",
    "velvet",
    "vivid",
    "walnut",
    "whale",
    "willow",
    "windy",
    "witty",
    "yarrow",
    "zesty",
)


def _coerce_positive_int(value: int | None, *, minimum: int, maximum: int | None = None) -> int:
    """Clamp ``value`` to the provided inclusive bounds."""
    if value is None:
        value = minimum
    value = int(value)
    if value < minimum:
        value = minimum
    if maximum is not None and value > maximum:
        value = maximum
    return value


def _ensure_category(part: list[str], pool: Sequence[str], mask: Iterable[bool]) -> None:
    """Force at least one character from ``pool`` into ``part`` where mask indicates availability."""
    indexes = [idx for idx, allowed in enumerate(mask) if allowed]
    if not indexes:
        # Fallback to any position if mask filtered everything (should not normally happen).
        indexes = list(range(len(part)))
    part[indexes[secrets.randbelow(len(indexes))]] = secrets.choice(pool)


def _generate_segment(length: int) -> str:
    """Generate a single Apple-style password segment."""
    part: list[str] = []
    has_digit = has_lower = has_upper = False
    for _ in range(length):
        ch = secrets.choice(_APPLE_CHARSET)
        part.append(ch)
        if ch.isdigit():
            has_digit = True
        elif ch.isupper():
            has_upper = True
        else:
            has_lower = True

    if not has_digit:
        _ensure_category(part, _UNAMBIGUOUS_DIGITS, (not c.isdigit() for c in part))
    if not has_lower:
        _ensure_category(part, _LOWER, (not c.islower() for c in part))
    if not has_upper:
        _ensure_category(part, _UPPER, (not c.isupper() for c in part))

    return "".join(part)


def generate_apple_style_password(
    *,
    length: int = 20,
    segments: int | None = None,
    segment_length: int = _DEFAULT_SEGMENT_LENGTH,
) -> str:
    """Create an Apple-like password comprised of hyphenated segments."""
    segment_length = _coerce_positive_int(segment_length, minimum=3, maximum=8)
    if segments is None:
        approx = max(length, segment_length)
        ideal = round(approx / (segment_length + 1))  # account for hyphen separators
        segments = _coerce_positive_int(ideal or _MIN_SEGMENTS, minimum=_MIN_SEGMENTS, maximum=_MAX_SEGMENTS)
    else:
        segments = _coerce_positive_int(segments, minimum=_MIN_SEGMENTS, maximum=_MAX_SEGMENTS)

    return "-".join(_generate_segment(segment_length) for _ in range(segments))


def generate_memorable_password(
    *,
    num_words: int = _DEFAULT_WORDS,
    separator: str = _DEFAULT_SEPARATOR,
    suffix_digits: int = _DEFAULT_SUFFIX_DIGITS,
    capitalize: bool = True,
) -> str:
    """Generate a short passphrase geared towards memorability."""
    num_words = _coerce_positive_int(num_words, minimum=2, maximum=_MAX_WORDS)
    words = [secrets.choice(_MEMORABLE_WORDS) for _ in range(num_words)]
    if capitalize:
        words = [w.capitalize() for w in words]
    password = separator.join(words)
    if suffix_digits > 0:
        suffix_digits = _coerce_positive_int(suffix_digits, minimum=1, maximum=4)
        digits = "".join(secrets.choice(_UNAMBIGUOUS_DIGITS) for _ in range(suffix_digits))
        if separator:
            password = f"{password}{separator}{digits}"
        else:
            password = f"{password}{digits}"
    return password


def generate_high_entropy_password(*, length: int = 20, alphabet: str | None = None) -> str:
    """Fallback generator mirroring the legacy behaviour."""
    length = _coerce_positive_int(length, minimum=8, maximum=256)
    alphabet = alphabet or (string.ascii_letters + string.digits + string.punctuation)
    return "".join(secrets.choice(alphabet) for _ in range(length))


def generate_password(
    *,
    length: int = 20,
    style: str = "apple",
    **kwargs,
) -> str:
    """Generate a password according to the requested style."""
    style_normalised = (style or "apple").strip().lower()
    if style_normalised in {"apple", "friendly", "default"}:
        return generate_apple_style_password(
            length=length,
            segments=kwargs.get("segments"),
            segment_length=kwargs.get("segment_length", _DEFAULT_SEGMENT_LENGTH),
        )
    if style_normalised in {"memorable", "words", "passphrase"}:
        return generate_memorable_password(
            num_words=kwargs.get("num_words", max(3, min(_MAX_WORDS, length // 5))),
            separator=kwargs.get("separator", _DEFAULT_SEPARATOR),
            suffix_digits=kwargs.get("suffix_digits", _DEFAULT_SUFFIX_DIGITS),
            capitalize=kwargs.get("capitalize", True),
        )
    return generate_high_entropy_password(length=length, alphabet=kwargs.get("alphabet"))


__all__ = [
    "generate_password",
    "generate_apple_style_password",
    "generate_memorable_password",
    "generate_high_entropy_password",
]
