# retoor <retoor@molodetz.nl>

from __future__ import annotations

import re
from datetime import datetime
from typing import Any

from .schema import InteractionRequest, WidgetModel, flatten_widgets

YES = frozenset({"y", "yes", "true", "1", "ok", "confirm", "ja", "oui", "j"})
NO = frozenset({"n", "no", "false", "0", "nee", "non"})
CANCEL = frozenset({"cancel", "c", "abort", "quit", "annuleren", "stop"})
DATE_FORMATS = (
    "%d/%m/%Y",
    "%d-%m-%Y",
    "%d.%m.%Y",
    "%d/%m/%y",
    "%d-%m-%y",
    "%Y-%m-%d",
)


def parse_plain_reply(
    request: InteractionRequest, text: str
) -> tuple[str, dict[str, Any], str | None]:
    raw = (text or "").strip()
    if not raw:
        return "error", {}, "Empty reply."
    lower = raw.lower()
    if lower in CANCEL:
        return "cancelled", {}, None

    widgets = [w for w in flatten_widgets(request.widgets) if w.type != "//"]
    if not widgets:
        return "submitted", {}, None

    if len(widgets) == 1:
        return _parse_single(widgets[0], raw, strict=True)

    if not _looks_like_multi_answer(raw, widgets):
        return "error", {}, "Not a structured answer for the open form."

    values: dict[str, Any] = {}
    chunks = [part.strip() for part in re.split(r"[;\n]+", raw) if part.strip()]
    if len(chunks) == 1 and "," in chunks[0] and len(widgets) > 1:
        chunks = [part.strip() for part in chunks[0].split(",") if part.strip()]
    if len(chunks) != len(widgets):
        return (
            "error",
            {},
            f"Expected {len(widgets)} values (one per field), got {len(chunks)}.",
        )
    for widget, chunk in zip(widgets, chunks):
        status, partial, err = _parse_single(widget, chunk, strict=True)
        if status != "submitted":
            return status, {}, err
        values.update(partial)
    return "submitted", values, None


def _looks_like_multi_answer(raw: str, widgets: list[WidgetModel]) -> bool:
    if ";" in raw or "\n" in raw:
        return True
    if "," in raw and len(widgets) > 1:
        return True
    return False


def _parse_single(
    widget: WidgetModel, raw: str, *, strict: bool = False
) -> tuple[str, dict[str, Any], str | None]:
    text = raw.strip()
    lower = text.lower()
    if lower in CANCEL:
        return "cancelled", {}, None
    name = widget.name
    if widget.type == "confirm":
        if lower in YES:
            return "submitted", {name: True}, None
        if lower in NO:
            return "submitted", {name: False}, None
        return "error", {}, "Reply yes/no (or ja/nee)."
    if widget.type in ("choice", "select"):
        value = _match_option(widget, text)
        if value is None:
            return "error", {}, f"Unknown option for {name}."
        return "submitted", {name: value}, None
    if widget.type == "choice_multi":
        parts = [p.strip() for p in re.split(r"[,\s]+", text) if p.strip()]
        values: list[str] = []
        for part in parts:
            matched = _match_option(widget, part)
            if matched is None:
                return "error", {}, f"Unknown option {part!r} for {name}."
            if matched not in values:
                values.append(matched)
        if widget.required and not values:
            return "error", {}, f"{name} requires at least one option."
        return "submitted", {name: values}, None
    if widget.type == "number":
        if strict and not re.fullmatch(r"[+-]?\d+(?:[.,]\d+)?", text):
            return "error", {}, f"{name} must be a number."
        normalized = text.replace(",", ".")
        try:
            number = float(normalized) if "." in normalized else int(normalized)
        except ValueError:
            return "error", {}, f"{name} must be a number."
        if widget.min is not None and number < widget.min:
            return "error", {}, f"{name} must be ≥ {widget.min}."
        if widget.max is not None and number > widget.max:
            return "error", {}, f"{name} must be ≤ {widget.max}."
        return "submitted", {name: number}, None
    if widget.type == "date":
        if strict and not _looks_like_date(text):
            return "error", {}, f"{name} must be a date as DD/MM/YYYY."
        european = _parse_date_european(text)
        if european is None:
            return "error", {}, f"{name} must be a date as DD/MM/YYYY."
        return "submitted", {name: european}, None
    if widget.type == "text":
        if strict and _looks_like_command(text):
            return "error", {}, f"{name} does not look like a field answer."
        if widget.max_length and len(text) > widget.max_length:
            return "error", {}, f"{name} exceeds max length {widget.max_length}."
        if widget.required and not text:
            return "error", {}, f"{name} is required."
        return "submitted", {name: text}, None
    return "error", {}, f"Unsupported widget type {widget.type}."


def _looks_like_date(text: str) -> bool:
    return bool(
        re.fullmatch(r"\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}", text.strip())
        or re.fullmatch(r"\d{4}-\d{2}-\d{2}", text.strip())
    )


def _looks_like_command(text: str) -> bool:
    value = text.strip().lower()
    if not value:
        return False
    if value.startswith("/"):
        return True
    starters = (
        "show me ",
        "laat ",
        "geef ",
        "toon ",
        "please ",
        "can you ",
        "could you ",
        "i want ",
        "ik wil ",
        "volgende",
        "next ",
    )
    return any(value.startswith(prefix) for prefix in starters)


def _parse_date_european(text: str) -> str | None:
    value = text.strip()
    for fmt in DATE_FORMATS:
        try:
            return datetime.strptime(value, fmt).strftime("%d/%m/%Y")
        except ValueError:
            continue
    return None


def _match_option(widget: WidgetModel, text: str) -> str | None:
    raw = text.strip()
    if raw.isdigit():
        index = int(raw)
        if 1 <= index <= len(widget.options):
            option = widget.options[index - 1]
            if not option.disabled:
                return option.value
    lower = raw.lower()
    for option in widget.options:
        if option.disabled:
            continue
        if option.value == raw or option.value.lower() == lower:
            return option.value
        if option.label.lower() == lower:
            return option.value
    return None
