"""Normalize Stock Oracle's filing-event vocabulary to the strategy's vocabulary.

Background
----------
When the rule-based parser cannot classify an 8-K (no exhibit text, low
confidence, etc.), `apps/pipeline/event_parser/main.py` falls back to the
Oracle filings-events API. Oracle's `event_type` strings are richer than
``ParserEventOutput.event_type`` — values like ``earnings_result``,
``shareholder_vote``, ``regulation_fd``, ``financial_obligation`` get written
into the ``events.event_type`` column unchanged, and the strategy's engine
filters silently drop them.

This module provides a *conservative* mapping from Oracle's vocabulary to the
strategy's: only synonyms and clear item-code parallels are mapped. Ambiguous
values are left as-is so the failure mode is honest (row drops out of the
strategy filter) rather than silently re-tagged.

Where possible, prefer ``normalize_oracle_event(value, item_number=...)``,
which routes through the existing 8-K item-code classifier in
``libs.parser.rule_parser`` for maximum consistency with the rule-based path.
"""
from __future__ import annotations

# ---------------------------------------------------------------------------
# Oracle vocabulary -> strategy vocabulary
#
# Conservative mapping. Each row needs an unambiguous synonym justification.
# When in doubt we leave the value untouched and let the engine filter drop
# it — that's louder and easier to debug than a quiet re-tag.
#
# Strategy vocabulary (libs/schemas/types.py ParserEventOutput.event_type):
#   earnings_release, guidance_update, material_contract, management_change,
#   other_material_event, regulatory_or_approval, capital_markets_or_financing,
#   litigation_or_investigation, unknown
#
# Strategy *engine filter* hot path uses 4-5 of these (libs/backtest/scoring.py
# and selector.py): earnings_release, guidance_update, material_contract,
# other_material_event, management_change.
# ---------------------------------------------------------------------------
_ORACLE_TO_STRATEGY: dict[str, str] = {
    # --- identity passes (already aligned) ---
    "earnings_release": "earnings_release",
    "guidance_update": "guidance_update",
    "material_contract": "material_contract",
    "management_change": "management_change",
    "other_material_event": "other_material_event",

    # --- earnings synonyms ---
    # Oracle's actual 412 mislabeled rows are "earnings_result"; "earnings_announcement"
    # is added defensively for any future Oracle vocabulary drift.
    "earnings_result": "earnings_release",
    "earnings_announcement": "earnings_release",
    "earnings": "earnings_release",

    # --- guidance synonyms ---
    "guidance_revision": "guidance_update",
    "guidance_change": "guidance_update",
    # Reg FD (Item 7.01): the rule_parser already maps Item 7.01 -> guidance_update,
    # so we keep parity. Reg FD often (but not always) carries guidance; this is the
    # one debatable choice and is intentional for consistency with the item-code path.
    "regulation_fd": "guidance_update",
    "regulation_fd_disclosure": "guidance_update",

    # --- material contract synonyms (Item 1.01 wording variants) ---
    "material_definitive_agreement": "material_contract",
    "definitive_agreement": "material_contract",

    # --- other_material_event (Item 5.07, 2.01, 1.03 etc.) ---
    # Item 5.07 — submission of matters to a vote of security holders.
    "shareholder_vote": "other_material_event",
    # Item 2.01 — completion of acquisition or disposition of assets.
    "acquisition_disposition": "other_material_event",
    "acquisition": "other_material_event",
    "disposition": "other_material_event",
    # Item 1.03 — bankruptcy or receivership.
    "bankruptcy": "other_material_event",
    # Generic "Other Events" (Item 8.01) — rule_parser maps 8.01 -> other_material_event.
    "other_events": "other_material_event",
    "other": "other_material_event",
}


def _canonicalize(value: str) -> str:
    """Lowercase, trim, collapse separators so 'Earnings Result', 'earnings-result',
    and 'EARNINGS_RESULT' all hit the same map key."""
    s = value.strip().lower()
    # unify separators
    s = s.replace("-", "_").replace(" ", "_").replace("/", "_")
    # collapse repeated underscores
    while "__" in s:
        s = s.replace("__", "_")
    return s.strip("_")


def normalize_oracle_event_type(oracle_value: str | None) -> str:
    """Map an Oracle ``event_type`` string to the strategy's vocabulary.

    Behavior:
    - ``None`` / empty / whitespace-only input -> ``"unknown"``.
    - Mapped values returned in canonical strategy form.
    - Unmapped values are returned untouched (preserving original casing/wording)
      so the engine filter drops them visibly rather than silently re-tagging.

    Examples:
        >>> normalize_oracle_event_type("earnings_result")
        'earnings_release'
        >>> normalize_oracle_event_type("Earnings Result")
        'earnings_release'
        >>> normalize_oracle_event_type("financial_obligation")
        'financial_obligation'
        >>> normalize_oracle_event_type(None)
        'unknown'
    """
    if oracle_value is None:
        return "unknown"
    if not isinstance(oracle_value, str):
        return "unknown"
    if not oracle_value.strip():
        return "unknown"

    key = _canonicalize(oracle_value)
    if key in _ORACLE_TO_STRATEGY:
        return _ORACLE_TO_STRATEGY[key]
    # Honest pass-through: unmapped Oracle vocab is kept verbatim.
    return oracle_value


def normalize_oracle_event(
    oracle_value: str | None,
    item_number: str | None = None,
) -> str:
    """Stronger normalizer that prefers 8-K ``item_number`` when available.

    The existing rule-based parser already maps SEC 8-K item codes to the
    strategy's vocabulary in ``libs.parser.rule_parser._ITEM_TO_EVENT_TYPE``.
    Routing Oracle-fallback rows through that classifier gives us strict
    parity with the rule-based path: a 2.02-tagged Oracle row becomes
    ``earnings_release`` *exactly* like a rule-parsed row would.

    Falls back to the string synonym map when the item code is absent or
    classifies as ``"unknown"``.
    """
    # Prefer item-code routing — already battle-tested via rule_parser.
    if item_number:
        # Local import keeps this module free of heavy parser deps when only
        # the string-only API is used (tests, lightweight callers).
        from libs.parser.rule_parser import _classify_event_type

        mapped = _classify_event_type([item_number])
        if mapped != "unknown":
            return mapped

    return normalize_oracle_event_type(oracle_value)


__all__ = [
    "normalize_oracle_event_type",
    "normalize_oracle_event",
]
