"""Deterministic candidate ranking (CONTRACT §5, tool ``get_candidates``).

Given one planned line and the invoice set, return ranked invoice candidates.
Scoring is fully deterministic — client-match weight, amount closeness, date
proximity and description similarity (``difflib``). No model is ever called.
"""

from __future__ import annotations

from difflib import SequenceMatcher
from typing import Iterable

from .models import Candidate, Invoice, PlannedLine

# Score weights (sum to 1.0). Client match dominates, then amount closeness.
_W_CLIENT = 0.50
_W_AMOUNT = 0.30
_W_DESC = 0.15
_W_DATE = 0.05

# Mid-month anchor used for the (minor) date-proximity tiebreaker: planned lines
# carry a month but no day, so invoices near mid-month rank marginally higher.
_MID_MONTH_DAY = 15


def _normalise(text: str) -> str:
    return " ".join(str(text).strip().lower().split())


def _desc_similarity(planned: PlannedLine, invoice: Invoice) -> float:
    """Best fuzzy ratio of the planning name against invoice text fields."""
    name = _normalise(planned.client_name)
    best = 0.0
    for other in (invoice.customer, invoice.description):
        if not other:
            continue
        ratio = SequenceMatcher(None, name, _normalise(other)).ratio()
        best = max(best, ratio)
    return round(best, 4)


def _amount_closeness(planned_amount: float, invoiced: float) -> float:
    """1.0 for an exact amount, decaying to 0.0 as the gap grows."""
    base = max(abs(planned_amount), 1.0)
    return max(0.0, 1.0 - abs(planned_amount - invoiced) / base)


def _date_closeness(day: int) -> float:
    return max(0.0, 1.0 - abs(day - _MID_MONTH_DAY) / _MID_MONTH_DAY)


def get_candidates(
    planned_line: PlannedLine, invoices: Iterable[Invoice]
) -> list[Candidate]:
    """Return invoice candidates for ``planned_line`` ranked best-first.

    Ranking is stable and deterministic: primary key is ``score`` (desc), with
    ``abs(amount_delta)`` then ``invoice_id`` as tiebreakers so equal-score ties
    resolve reproducibly.
    """
    candidates: list[Candidate] = []
    for inv in invoices:
        client_match = (
            planned_line.odoo_customer_id is not None
            and inv.odoo_customer_id == planned_line.odoo_customer_id
        )
        amount_delta = round(inv.amount_untaxed - planned_line.planned_amount, 2)
        desc_sim = _desc_similarity(planned_line, inv)
        amount_close = _amount_closeness(
            planned_line.planned_amount, inv.amount_untaxed
        )
        date_close = _date_closeness(inv.invoice_date.day)
        score = (
            _W_CLIENT * (1.0 if client_match else 0.0)
            + _W_AMOUNT * amount_close
            + _W_DESC * desc_sim
            + _W_DATE * date_close
        )
        candidates.append(
            Candidate(
                invoice_id=inv.odoo_id,
                amount_delta=amount_delta,
                date_proximity_days=abs(inv.invoice_date.day - _MID_MONTH_DAY),
                desc_similarity=desc_sim,
                score=round(score, 6),
                number=inv.number,
                customer=inv.customer,
                amount_untaxed=inv.amount_untaxed,
                client_match=client_match,
            )
        )

    candidates.sort(key=lambda c: (-c.score, abs(c.amount_delta), c.invoice_id))
    return candidates
