"""Analytic module: prime decomposition of D(n) and its Dirichlet series.

The arithmetic denominator D(n) = gcd(M(n+1), (n+1)!) * gcd(L(n+1), (n+1)!)
admits a prime factorization D(n) = prod p^{v_p(D(n))} whose p-adic
valuations v_p exhibit striking periodicity.

This module provides:
- Prime factorization of D(n) and the full D-sequence
- Divisor count function sigma_0
- Normalized growth analysis D(n) / (sigma_0(n+1) * ln(n))
- Dirichlet partial sums sum_{n=1}^{N} D(n) / n^s
- Euler factor sequences v_p(D(n)) for fixed primes p
- Abscissa of convergence estimation for the Dirichlet series
- Theorem Z: support periodicity of v_p(D(n))

Key observations:
    v_2(D(n)): period 3 pattern [0, 1, 0, 0, 1, 0, 0, 2, 0, ...]
    v_p(D(n)): for prime p, nonzero only when (p-1) | n (approximately)
    D(n) is NOT multiplicative, but log D decomposes additively over primes

Theorem Z (Support Periodicity of p-adic valuations):
    For odd prime p, v_p(D(n)) > 0 if and only if n+1 >= p and either:
      (a) ord_2(p) | (n+1), i.e. p divides the Mersenne number M(n+1), or
      (b) alpha_L(p) | (n+1) and (n+1)/alpha_L(p) is odd, i.e. p divides L(n+1).
    For p = 2, v_2(D(n)) > 0 iff 3 | (n+1) and n >= 1.

    The support period tau_p is the minimal period of the indicator function
    I(k) = [ord_2(p)|k OR (alpha_L(p)|k AND k/alpha_L(p) is odd)].
    In general tau_p | lcm(ord_2(p), 2*alpha_L(p)).

    Proof sketch: D(n) = gcd(M(n+1),(n+1)!) * gcd(L(n+1),(n+1)!).
    The p-adic valuation decomposes as v_p(D(n)) = min(v_p(M(n+1)), v_p((n+1)!))
    + min(v_p(L(n+1)), v_p((n+1)!)). The first term is nonzero iff p | M(n+1)
    AND v_p((n+1)!) >= 1 (i.e. n+1 >= p). Since M(n+1) = 2^{n+1}-1, we have
    p | M(n+1) iff ord_2(p) | (n+1). The second term is nonzero iff p | L(n+1)
    AND n+1 >= p. By the Lucas divisibility law, for odd prime p, p | L(k) iff
    k/alpha_L(p) is a positive odd integer.
"""

import math
from collections import Counter
from typing import Dict, List, Optional

from .coupling import arithmetic_denominator
from .kaleidoscope import lucas, prime_factors


def prime_factorization_D(n: int) -> Dict[int, int]:
    """Return the prime factorization of D(n) as {p: v_p}.

    Parameters:
        n: positive integer

    Returns:
        Dictionary mapping prime p to its exponent v_p(D(n)).
        Returns {} if D(n) = 1.

    Examples:
        >>> prime_factorization_D(5)
        {2: 1, 3: 4}
        >>> prime_factorization_D(8)
        {2: 2, 7: 1}
    """
    if n < 1:
        raise ValueError(f"n must be >= 1, got {n}")
    D = arithmetic_denominator(n)[0]
    if D == 1:
        return {}
    factors = prime_factors(D)
    return dict(Counter(factors))


def D_sequence(N: int) -> List[int]:
    """Return [D(1), D(2), ..., D(N)].

    Parameters:
        N: positive integer, length of sequence

    Returns:
        List of D(n) values for n = 1, 2, ..., N.
    """
    if N < 1:
        raise ValueError(f"N must be >= 1, got {N}")
    return [arithmetic_denominator(n)[0] for n in range(1, N + 1)]


def divisor_count(n: int) -> int:
    """sigma_0(n): count of positive divisors of n.

    Parameters:
        n: positive integer

    Returns:
        Number of divisors of n.

    Examples:
        >>> divisor_count(12)
        6
        >>> divisor_count(7)
        2
    """
    if n < 1:
        raise ValueError(f"n must be >= 1, got {n}")
    count = 0
    # Only iterate up to sqrt(n)
    isqrt = int(math.isqrt(n))
    for d in range(1, isqrt + 1):
        if n % d == 0:
            count += 1
            if d != n // d:
                count += 1
    return count


def D_growth_normalized(N: int) -> List[float]:
    """Normalized growth ratio D(n) / (sigma_0(n+1) * ln(n)) for n=2..N.

    The conjectured asymptotic D(n) ~ sigma_0(n+1) * ln(n) would make
    this ratio converge to a constant.  In practice the ratio is highly
    variable because D(n) = 1 for many n (when M(n+1) and L(n+1) are
    coprime to (n+1)!).

    Parameters:
        N: upper bound (must be >= 2)

    Returns:
        List of ratios for n = 2, 3, ..., N (length N-1).
    """
    if N < 2:
        raise ValueError(f"N must be >= 2, got {N}")
    ratios = []
    for n in range(2, N + 1):
        D = arithmetic_denominator(n)[0]
        s0 = divisor_count(n + 1)
        denom = s0 * math.log(n)
        ratios.append(D / denom)
    return ratios


def dirichlet_partial(N: int, s: float) -> float:
    """Partial sum of the Dirichlet series: sum_{n=1}^{N} D(n) / n^s.

    Parameters:
        N: number of terms (>= 1)
        s: real exponent

    Returns:
        The partial sum as a float.

    Notes:
        For s large enough (s > abscissa of convergence), this converges
        as N -> infinity.  The abscissa depends on the growth rate of D(n).
    """
    if N < 1:
        raise ValueError(f"N must be >= 1, got {N}")
    total = 0.0
    for n in range(1, N + 1):
        D = arithmetic_denominator(n)[0]
        total += D / (n ** s)
    return total


def euler_factor_sequence(p: int, N: int) -> List[int]:
    """Return [v_p(D(1)), v_p(D(2)), ..., v_p(D(N))] for prime p.

    The p-adic valuation v_p(D(n)) measures how many times prime p
    divides D(n).  These sequences exhibit periodic or quasi-periodic
    structure:
        v_2: period 3 in the nonzero positions
        v_3: complex quasi-periodic structure
        v_5: period 4 pattern
        v_p (p >= 7): nonzero roughly every (p-1) terms

    Parameters:
        p: a prime number
        N: sequence length (>= 1)

    Returns:
        List of p-adic valuations of D(n) for n = 1..N.
    """
    if N < 1:
        raise ValueError(f"N must be >= 1, got {N}")
    if p < 2:
        raise ValueError(f"p must be a prime >= 2, got {p}")
    result = []
    for n in range(1, N + 1):
        D = arithmetic_denominator(n)[0]
        v = 0
        if D > 0:
            temp = D
            while temp % p == 0:
                v += 1
                temp //= p
        result.append(v)
    return result


def abscissa_estimate(N: int) -> float:
    """Estimate the abscissa of convergence of sum D(n)/n^s.

    Uses the Cauchy-Hadamard criterion: the abscissa sigma_c satisfies
        sigma_c = limsup_{n->inf} ln(D(n)) / ln(n).

    Since D(n) can be 1 for many n, and occasionally very large, we
    compute the running maximum of ln(D(n))/ln(n) over n = 2..N.

    Parameters:
        N: number of terms to use (>= 2)

    Returns:
        Estimated abscissa of convergence (float).
    """
    if N < 2:
        raise ValueError(f"N must be >= 2, got {N}")
    max_ratio = 0.0
    for n in range(2, N + 1):
        D = arithmetic_denominator(n)[0]
        if D > 1:
            ratio = math.log(D) / math.log(n)
            if ratio > max_ratio:
                max_ratio = ratio
        # D=1 contributes ratio=0, doesn't affect limsup
    return max_ratio


# ── Theorem Z: support periodicity of v_p(D(n)) ──────────────────────────────

def ord_2(p: int) -> Optional[int]:
    """Multiplicative order of 2 modulo p.

    The smallest positive integer k such that 2^k = 1 (mod p).
    Returns None for p = 2 (since 2 mod 2 = 0, order is undefined).

    Parameters:
        p: an odd prime

    Returns:
        The multiplicative order, or None if p = 2.

    Examples:
        >>> ord_2(3)
        2
        >>> ord_2(7)
        3
        >>> ord_2(11)
        10
    """
    if p < 2:
        raise ValueError(f"p must be a prime >= 2, got {p}")
    if p == 2:
        return None
    k = 1
    val = 2 % p
    while val != 1:
        k += 1
        val = (val * 2) % p
    return k


def alpha_lucas(p: int) -> Optional[int]:
    """Rank of apparition of prime p in the Lucas sequence.

    The smallest positive integer k such that p divides L(k).
    Returns None if p never divides any Lucas number (this occurs
    for primes p = 5 (mod 10) that are congruent to +/-1 mod 5,
    specifically when the Legendre symbol (5/p) = 1 and the
    Wall-Sun-Sun conditions exclude Lucas divisibility).

    Parameters:
        p: a prime >= 2

    Returns:
        The rank of apparition, or None if p never divides L(k).

    Examples:
        >>> alpha_lucas(2)
        3
        >>> alpha_lucas(3)
        2
        >>> alpha_lucas(7)
        4
        >>> alpha_lucas(5) is None
        True
    """
    if p < 2:
        raise ValueError(f"p must be a prime >= 2, got {p}")
    # Search up to a generous bound; for primes p, alpha_L(p) <= 2(p+1)
    bound = max(4 * (p + 1), 200)
    for k in range(1, bound):
        if lucas(k) % p == 0:
            return k
    return None


def vp_support_period(p: int) -> int:
    """Support period tau_p of the p-adic valuation v_p(D(n)).

    Theorem Z: the binary sequence [v_p(D(n)) > 0] is eventually
    periodic with minimal period tau_p.  For p = 2, tau_2 = 3.
    For odd p with alpha_L(p) undefined, tau_p = ord_2(p).
    For odd p with both ord_2(p) and alpha_L(p) defined, tau_p is
    the minimal period of the indicator
        I(k) = [ord_2(p) | k] OR [alpha_L(p) | k AND k/alpha_L(p) odd].

    Parameters:
        p: a prime >= 2

    Returns:
        The support period tau_p.

    Examples:
        >>> vp_support_period(2)
        3
        >>> vp_support_period(5)
        4
        >>> vp_support_period(11)
        5
    """
    if p < 2:
        raise ValueError(f"p must be a prime >= 2, got {p}")
    if p == 2:
        return 3  # alpha_L(2) = 3; M(n+1) always odd

    o2 = ord_2(p)
    aL = alpha_lucas(p)

    if aL is None:
        # Only Mersenne contributes
        return o2

    # Full period is lcm(ord_2(p), 2*alpha_L(p))
    full_period = math.lcm(o2, 2 * aL)

    # Build one full period of the indicator and find minimal sub-period
    indicator = []
    for j in range(1, full_period + 1):
        mersenne_hit = (j % o2 == 0)
        lucas_hit = (j % aL == 0) and ((j // aL) % 2 == 1)
        indicator.append(1 if (mersenne_hit or lucas_hit) else 0)

    # Find minimal period dividing full_period
    for T in range(1, full_period + 1):
        if full_period % T != 0:
            continue
        valid = True
        for i in range(full_period):
            if indicator[i] != indicator[i % T]:
                valid = False
                break
        if valid:
            return T

    return full_period  # fallback (should not reach here)
