from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timedelta
import re

from dateparser.search import search_dates

from backend.action_intents import (
    BaseActionIntent,
    EMAIL_ADDRESS_RE,
    LEADING_NOISE_RE,
    QUOTED_TEXT_RE,
    apply_reference,
    build_reference_spec,
    detect_keyword_action,
    extract_quoted_value,
    merge_base_intent_fields,
    score_action_intent,
)
from backend.local_time import get_windows_localzone


_TIME_RE = re.compile(r"\b\d{1,2}[:.]\d{2}\b|\b\d{1,2}\s?(?:am|pm)\b|\b\d{1,2}h\b", re.IGNORECASE)
_DATE_TOKEN_RE = re.compile(
    r"\b(?:today|tomorrow|yesterday|next|this|last|monday|tuesday|wednesday|thursday|friday|saturday|sunday|"
    r"vandaag|morgen|gisteren|volgende|deze|vorige|maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag|"
    r"\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?)\b",
    re.IGNORECASE,
)

# ── Timezone-aware scheduling ────────────────────────────────────────────────
_TZ_ABBREVIATIONS: dict[str, str] = {
    "EST": "America/New_York", "EDT": "America/New_York",
    "CST": "America/Chicago", "CDT": "America/Chicago",
    "MST": "America/Denver", "MDT": "America/Denver",
    "PST": "America/Los_Angeles", "PDT": "America/Los_Angeles",
    "GMT": "Europe/London", "BST": "Europe/London",
    "CET": "Europe/Amsterdam", "CEST": "Europe/Amsterdam",
    "EET": "Europe/Helsinki", "EEST": "Europe/Helsinki",
    "IST": "Asia/Kolkata", "JST": "Asia/Tokyo",
    "AEST": "Australia/Sydney", "AEDT": "Australia/Sydney",
    "SGT": "Asia/Singapore", "HKT": "Asia/Hong_Kong",
}
_TZ_DETECT_RE = re.compile(
    r"\b(?:(" + "|".join(re.escape(k) for k in sorted(_TZ_ABBREVIATIONS, key=len, reverse=True)) +
    r")\s+(?:time|tijd)?|(?:their|hun|zijn|haar)\s+(?:time|tijd))\b",
    re.IGNORECASE,
)


def _detect_timezone(text: str) -> str | None:
    """Return an IANA timezone name if the user specified one, else None."""
    m = _TZ_DETECT_RE.search(text)
    if not m:
        return None
    abbr = (m.group(1) or "").upper()
    if abbr and abbr in _TZ_ABBREVIATIONS:
        return _TZ_ABBREVIATIONS[abbr]
    # "their time" / "hun tijd" — can't resolve without context
    return None


def _convert_to_timezone(dt: datetime, tz_name: str) -> datetime:
    """Convert a naive local datetime to the user's intended wall-clock in tz_name,
    then return the equivalent local datetime for calendar storage."""
    try:
        from zoneinfo import ZoneInfo
    except ImportError:
        return dt
    if dt.tzinfo is not None:
        return dt
    target_tz = ZoneInfo(tz_name)
    local_tz = get_windows_localzone()
    # User said "3pm EST" → interpret 3pm in target timezone, convert to local
    aware = dt.replace(tzinfo=target_tz)
    return aware.astimezone(local_tz).replace(tzinfo=None)
_CALENDAR_MARKERS_RE = re.compile(
    r"\b(calendar|agenda|agenda's|agendas|appointment|appointments|meeting|meetings|event|events|afspraak|afspraken|afpraak)\b",
    re.IGNORECASE,
)

_ACTION_TERMS = {
    "create": {
        "create", "schedule", "book", "plan", "add", "make", "set up",
        "maak", "maken", "inplan", "inplannen", "plan", "plannen",
        "boek", "boeken", "zet", "voeg toe", "plaats",
    },
    "update": {
        "update", "edit", "change", "move", "reschedule", "rename", "delay", "bring forward",
        "wijzig", "wijzigen", "bewerk", "bewerken", "verplaats", "verplaatsen",
        "verzet", "verzetten", "vervroeg", "vervroegen", "verlaat", "aanpas", "aanpassen",
    },
    "delete": {
        "delete", "remove", "cancel", "drop",
        "verwijder", "verwijderen", "annuleer", "annuleren", "schrap", "schrappen",
    },
}

_GENERIC_SUMMARY_WORDS = {
    "appointment", "event", "meeting", "calendar", "agenda", "afspraak", "afspraken",
}

_LEADING_NOISE_RE = re.compile(
    r"^(?:please|pls|can you|could you|will you|wil je|kun je|kun jij|zou je|graag|even)\s+",
    re.IGNORECASE,
)

_LOCATION_RE = re.compile(
    r"\b(?:location|locatie)\s*[:\-]?\s*([^,.;\n]+)",
    re.IGNORECASE,
)
_CORRECTION_PREFIX_RE = re.compile(
    r"^(?:nee|no|niet|ik\s+bedoel|bedoel\s+ik|i\s+mean)\s+",
    re.IGNORECASE,
)
_IMPLICIT_UPDATE_RE = re.compile(
    r"\b(?:locatie|location|titel|title|beschrijving|description|attendees|deelnemers)\b.*\b(?:toevoeg(?:en)?|add)\b"
    r"|\b(?:toevoeg(?:en)?|add)\b.*\b(?:locatie|location|titel|title|beschrijving|description|attendees|deelnemers)\b",
    re.IGNORECASE,
)


@dataclass
class AppointmentIntent(BaseActionIntent):
    summary: str = ""
    start: str = ""
    end: str = ""
    location: str = ""
    description: str = ""
    attendees: str = ""
    all_day: bool = False
    detected_languages: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return {
            "action": self.action,
            "summary": self.summary,
            "selection_query": self.selection_query,
            "reference_kind": self.reference_kind,
            "reference_ordinal": self.reference_ordinal,
            "reference_temporal_hint": self.reference_temporal_hint,
            "start": self.start,
            "end": self.end,
            "location": self.location,
            "description": self.description,
            "attendees": self.attendees,
            "all_day": self.all_day,
            "confidence": self.confidence,
            "missing_fields": list(self.missing_fields),
            "detected_languages": list(self.detected_languages),
            "used_llm_fallback": self.used_llm_fallback,
        }


def detect_calendar_action(text: str) -> str:
    detected = detect_keyword_action(text, _ACTION_TERMS)
    if detected:
        return detected
    raw = str(text or "").strip()
    if not raw:
        return ""
    if _CALENDAR_MARKERS_RE.search(raw) and _IMPLICIT_UPDATE_RE.search(raw):
        return "update"
    return ""


def parse_appointment_intent(message: str, reference: datetime | None = None) -> AppointmentIntent:
    raw = str(message or "").strip()
    reference = reference or datetime.now()
    intent = AppointmentIntent(action=detect_calendar_action(raw))
    if not raw:
        return validate_appointment_intent(intent)

    intent.attendees = _extract_attendees(raw)
    intent.location = _extract_location(raw)

    # Detect timezone specification (e.g. "3pm EST", "hun tijd")
    detected_tz = _detect_timezone(raw)

    if intent.action == "create":
        start_dt, end_dt, all_day, languages = _extract_create_range(raw, reference)
        if detected_tz and not all_day:
            if start_dt:
                start_dt = _convert_to_timezone(start_dt, detected_tz)
            if end_dt:
                end_dt = _convert_to_timezone(end_dt, detected_tz)
        if start_dt:
            intent.start = _format_event_datetime(start_dt, all_day)
        if end_dt:
            intent.end = _format_event_datetime(end_dt, all_day)
        intent.all_day = all_day
        intent.detected_languages = languages
        intent.summary = _extract_create_summary(raw)
    elif intent.action in {"update", "delete"}:
        selection_query, updated_text = _extract_selection_query(raw, intent.action)
        apply_reference(intent, build_reference_spec(raw, selection_query))
        intent.summary = _extract_updated_summary(updated_text)
        start_dt, end_dt, all_day, languages = _extract_update_range(updated_text, reference)
        if detected_tz and not all_day:
            if start_dt:
                start_dt = _convert_to_timezone(start_dt, detected_tz)
            if end_dt:
                end_dt = _convert_to_timezone(end_dt, detected_tz)
        if start_dt:
            intent.start = _format_event_datetime(start_dt, all_day)
        if end_dt:
            intent.end = _format_event_datetime(end_dt, all_day)
        intent.all_day = all_day
        intent.detected_languages = languages
    else:
        if _CALENDAR_MARKERS_RE.search(raw):
            apply_reference(intent, build_reference_spec(raw, _clean_selection_candidate(_strip_calendar_noise(raw))))

    return validate_appointment_intent(intent)


def validate_appointment_intent(intent: AppointmentIntent) -> AppointmentIntent:
    validated = AppointmentIntent(**intent.to_dict())
    validated.missing_fields = []

    if validated.action == "create":
        if not validated.summary:
            validated.missing_fields.append("summary")
        if not validated.start:
            validated.missing_fields.append("start")
        if validated.start and not validated.end:
            validated.missing_fields.append("end")
    elif validated.action == "update":
        if not validated.selection_query and validated.reference_ordinal is None:
            validated.missing_fields.append("selection_query")
        if not any([
            validated.summary,
            validated.start,
            validated.end,
            validated.location,
            validated.description,
        ]):
            validated.missing_fields.append("changes")
    elif validated.action == "delete":
        if not validated.selection_query and validated.reference_ordinal is None:
            validated.missing_fields.append("selection_query")

    validated.confidence = _score_intent(validated)
    return validated


def appointment_intent_to_create_payload(intent: AppointmentIntent) -> dict:
    return {
        "summary": intent.summary,
        "start": intent.start,
        "end": intent.end,
        "description": intent.description,
        "location": intent.location,
        "attendees": intent.attendees,
    }


def appointment_intent_to_update_directives(intent: AppointmentIntent) -> dict:
    return {
        "selection_query": intent.selection_query,
        "new_summary": intent.summary,
        "new_start": intent.start,
        "new_end": intent.end,
        "new_location": intent.location,
        "new_description": intent.description,
        "all_day": intent.all_day,
    }


def merge_appointment_intents(primary: AppointmentIntent, fallback: AppointmentIntent) -> AppointmentIntent:
    merged = AppointmentIntent(
        **merge_base_intent_fields(primary, fallback),
        summary=primary.summary or fallback.summary,
        start=primary.start or fallback.start,
        end=primary.end or fallback.end,
        location=primary.location or fallback.location,
        description=primary.description or fallback.description,
        attendees=primary.attendees or fallback.attendees,
        all_day=primary.all_day or fallback.all_day,
        detected_languages=list(dict.fromkeys(primary.detected_languages + fallback.detected_languages)),
    )
    return validate_appointment_intent(merged)


def appointment_intent_from_classifier(
    params: dict,
    action: str,
    message: str,
    reference: datetime | None = None,
) -> AppointmentIntent:
    """Build an AppointmentIntent directly from LLM classifier parameters.

    This is the primary path — the classifier already understands context like
    'ipv', 'instead of', relative dates, etc.  Regex parsing is the fallback.
    """
    reference = reference or datetime.now()
    intent = AppointmentIntent(action=action)

    target = str(params.get("target_summary") or "").strip()
    new_start = str(params.get("new_start") or "").strip()
    new_end = str(params.get("new_end") or "").strip()
    new_summary = str(params.get("new_summary") or params.get("summary") or "").strip()
    location = str(params.get("location") or "").strip()
    description = str(params.get("description") or "").strip()
    attendees = str(params.get("attendees") or "").strip()

    if action in ("update", "delete"):
        intent.selection_query = target
        apply_reference(intent, build_reference_spec(message, target or message))
        if new_summary:
            intent.summary = new_summary
    elif action == "create":
        intent.summary = new_summary or target

    if new_start:
        intent.start = new_start
    if new_end:
        intent.end = new_end
    if location:
        intent.location = location
    if description:
        intent.description = description
    if attendees:
        intent.attendees = attendees

    return validate_appointment_intent(intent)


def appointment_intent_from_llm_payload(payload: dict) -> AppointmentIntent:
    selection_query = str(payload.get("selection_query") or payload.get("target_event") or "").strip()
    reference_kind = str(payload.get("reference_kind") or "").strip()
    reference_ordinal = payload.get("reference_ordinal") if payload.get("reference_ordinal") is not None else None
    reference_temporal_hint = str(payload.get("reference_temporal_hint") or "").strip()
    if selection_query and not reference_kind and reference_ordinal is None and not reference_temporal_hint:
        inferred = build_reference_spec(selection_query, selection_query)
        reference_kind = inferred.kind
        reference_ordinal = inferred.ordinal
        reference_temporal_hint = inferred.temporal_hint
    normalized = AppointmentIntent(
        action=str(payload.get("action") or "").strip().lower(),
        summary=str(payload.get("summary") or "").strip(),
        selection_query=selection_query,
        reference_kind=reference_kind,
        reference_ordinal=reference_ordinal,
        reference_temporal_hint=reference_temporal_hint,
        start=str(payload.get("start") or "").strip(),
        end=str(payload.get("end") or "").strip(),
        location=str(payload.get("location") or "").strip(),
        description=str(payload.get("description") or "").strip(),
        attendees=str(payload.get("attendees") or "").strip(),
        all_day=bool(payload.get("all_day", False)),
        detected_languages=[str(lang).strip() for lang in payload.get("detected_languages", []) if str(lang).strip()],
        used_llm_fallback=True,
    )
    return validate_appointment_intent(normalized)


def _extract_attendees(text: str) -> str:
    emails: list[str] = []
    for match in EMAIL_ADDRESS_RE.findall(str(text or "")):
        if match not in emails:
            emails.append(match)
    return ", ".join(emails)


def _extract_location(text: str) -> str:
    raw = str(text or "")
    prefixed_match = re.search(
        r"\b(?:location|locatie)\s+(?:toevoeg(?:en)?|add)(?:\s+(?:aan|to)\s+[^:]+)?\s*[:\-]\s*([^,.;\n]+)",
        raw,
        re.IGNORECASE,
    )
    if prefixed_match:
        return prefixed_match.group(1).strip(" .,;:!?'\"")
    trailing_match = re.search(
        r"\b(?:location|locatie)\s*[:\-]?\s*([^,.;\n]+?)\s+(?:toevoeg(?:en)?|add)\b",
        raw,
        re.IGNORECASE,
    )
    if trailing_match:
        return trailing_match.group(1).strip(" .,;:!?'\"")
    match = _LOCATION_RE.search(raw)
    if not match:
        return ""
    value = match.group(1).strip(" .,;:!?'\"")
    value = re.sub(r"\b(?:toevoeg(?:en)?|add)\b$", "", value, flags=re.IGNORECASE).strip(" .,;:!?'\"")
    return value


def _extract_create_summary(text: str) -> str:
    quoted = _extract_quoted_value(text)
    if quoted:
        return quoted
    stripped = _strip_calendar_noise(text, remove_dates=True)
    return _clean_summary_candidate(stripped)


def _extract_updated_summary(text: str) -> str:
    marker_match = re.search(r"\b(?:renamed to|rename to|titel|title|genaamd|called)\b\s+(.+)$", str(text or ""), re.IGNORECASE)
    if marker_match:
        return _clean_summary_candidate(marker_match.group(1))
    quoted = _extract_quoted_value(text)
    if quoted and len(quoted.split()) <= 12:
        return quoted
    return ""


def _extract_selection_query(text: str, action: str) -> tuple[str, str]:
    raw = str(text or "").strip()
    quoted_match = QUOTED_TEXT_RE.search(raw)
    quoted = _extract_quoted_value(raw)
    if quoted:
        if action == "update" and quoted_match:
            updated_text = raw[quoted_match.end():].strip()
            if updated_text:
                return quoted, updated_text
        return quoted, raw

    if action == "update":
        move_match = re.search(r"\b(?:to|naar)\b\s+(.+)$", raw, re.IGNORECASE)
        if move_match:
            left = raw[:move_match.start()].strip()
            return _clean_selection_candidate(left), move_match.group(1).strip()
        reference_patterns = [
            re.compile(
                r"\b(?:aan|to|for|voor)\s+(?:de|het|mijn|my)?\s*(?:afs?praak|meeting|event)\s+(.+?)(?=\s+\b(?:locatie|location|titel|title|beschrijving|description|attendees|deelnemers|met|with|toevoeg(?:en)?|add)\b|[:.,;!?]|$)",
                re.IGNORECASE,
            ),
            re.compile(
                r"^(?:nee|no|niet|ik\s+bedoel|bedoel\s+ik|i\s+mean)\s+(?:aan\s+)?(?:de|het|mijn|my)?\s*(?:afs?praak|meeting|event)\s+(.+)$",
                re.IGNORECASE,
            ),
        ]
        for pattern in reference_patterns:
            match = pattern.search(raw)
            if not match:
                continue
            selection = _clean_selection_candidate(match.group(1))
            if selection:
                return selection, raw
    return _clean_selection_candidate(raw), raw


def _extract_create_range(text: str, reference: datetime) -> tuple[datetime | None, datetime | None, bool, list[str]]:
    return _extract_range_from_segment(text, reference)


def _extract_update_range(text: str, reference: datetime) -> tuple[datetime | None, datetime | None, bool, list[str]]:
    return _extract_range_from_segment(text, reference)


def _extract_range_from_segment(text: str, reference: datetime) -> tuple[datetime | None, datetime | None, bool, list[str]]:
    raw = str(text or "").strip()
    if not raw:
        return None, None, False, []

    matches = _search_date_matches(raw, reference)
    languages = [lang for _, _, lang in matches if lang]
    if not matches:
        return None, None, False, []

    anchored_dates = [(fragment, dt) for fragment, dt, _ in matches if _looks_like_date_anchor(fragment)]
    clock_points = [(fragment, dt) for fragment, dt, _ in matches if _looks_like_clock_time(fragment)]
    datetime_points = [(fragment, dt) for fragment, dt, _ in matches if _looks_like_full_datetime(fragment)]

    if len(datetime_points) >= 2:
        start_dt = datetime_points[0][1]
        end_dt = datetime_points[1][1]
        if end_dt <= start_dt:
            end_dt = start_dt + timedelta(hours=1)
        return start_dt, end_dt, False, languages

    if datetime_points:
        start_dt = datetime_points[0][1]
        if len(clock_points) >= 2 and anchored_dates:
            anchor = anchored_dates[0][1]
            first_time = clock_points[0][1]
            second_time = clock_points[1][1]
            start_dt = anchor.replace(hour=first_time.hour, minute=first_time.minute)
            end_dt = anchor.replace(hour=second_time.hour, minute=second_time.minute)
            if end_dt <= start_dt:
                end_dt = start_dt + timedelta(hours=1)
            return start_dt, end_dt, False, languages
        return start_dt, start_dt + timedelta(hours=1), False, languages

    if anchored_dates and len(clock_points) >= 2:
        anchor = anchored_dates[0][1]
        start_dt = anchor.replace(hour=clock_points[0][1].hour, minute=clock_points[0][1].minute)
        end_dt = anchor.replace(hour=clock_points[1][1].hour, minute=clock_points[1][1].minute)
        if end_dt <= start_dt:
            end_dt = start_dt + timedelta(hours=1)
        return start_dt, end_dt, False, languages

    if anchored_dates and len(clock_points) == 1:
        anchor = anchored_dates[0][1]
        start_dt = anchor.replace(hour=clock_points[0][1].hour, minute=clock_points[0][1].minute)
        return start_dt, start_dt + timedelta(hours=1), False, languages

    if anchored_dates:
        anchor = anchored_dates[0][1].replace(hour=0, minute=0, second=0, microsecond=0)
        return anchor, anchor + timedelta(days=1), True, languages

    if len(clock_points) >= 2:
        today = reference.replace(hour=0, minute=0, second=0, microsecond=0)
        start_dt = today.replace(hour=clock_points[0][1].hour, minute=clock_points[0][1].minute)
        end_dt = today.replace(hour=clock_points[1][1].hour, minute=clock_points[1][1].minute)
        if end_dt <= start_dt:
            end_dt = start_dt + timedelta(hours=1)
        return start_dt, end_dt, False, languages

    return None, None, False, languages


def _search_date_matches(text: str, reference: datetime) -> list[tuple[str, datetime, str]]:
    settings = {
        "PREFER_DATES_FROM": "future",
        "RELATIVE_BASE": reference,
        "RETURN_AS_TIMEZONE_AWARE": False,
    }
    found = search_dates(text, settings=settings, add_detected_language=True) or []
    normalized: list[tuple[str, datetime, str]] = []
    for item in found:
        if len(item) == 3:
            fragment, value, language = item
        else:
            fragment, value = item[0], item[1]
            language = ""
        fragment_text = str(fragment or "").strip()
        if not fragment_text:
            continue
        normalized.append((fragment_text, value, str(language or "")))
    return normalized


def _looks_like_clock_time(fragment: str) -> bool:
    return bool(_TIME_RE.search(str(fragment or "")))


def _looks_like_date_anchor(fragment: str) -> bool:
    value = str(fragment or "")
    return bool(_DATE_TOKEN_RE.search(value))


def _looks_like_full_datetime(fragment: str) -> bool:
    value = str(fragment or "")
    return _looks_like_date_anchor(value) and _looks_like_clock_time(value)


def _format_event_datetime(value: datetime, all_day: bool) -> str:
    if all_day:
        return value.strftime("%Y-%m-%d")
    return value.strftime("%Y-%m-%dT%H:%M:%S")


def _extract_quoted_value(text: str) -> str:
    return extract_quoted_value(text)


def _strip_calendar_noise(text: str, remove_dates: bool = False) -> str:
    value = str(text or "").strip()
    value = _LEADING_NOISE_RE.sub("", value)
    for terms in _ACTION_TERMS.values():
        for token in sorted(terms, key=len, reverse=True):
            value = re.sub(rf"\b{re.escape(token)}\b", " ", value, flags=re.IGNORECASE)
    value = _CALENDAR_MARKERS_RE.sub(" ", value)
    value = EMAIL_ADDRESS_RE.sub(" ", value)
    value = _LOCATION_RE.sub(" ", value)
    if remove_dates:
        for fragment, _, _ in _search_date_matches(value, datetime.now()):
            value = re.sub(re.escape(fragment), " ", value, flags=re.IGNORECASE)
    value = re.sub(r"\b(?:from|to|naar|van|at|om|on|op|for|voor|in|de|het|een|the|my|mijn)\b", " ", value, flags=re.IGNORECASE)
    value = re.sub(r"\s+", " ", value)
    return value.strip(" .,;:!?\"'")


def _clean_summary_candidate(text: str) -> str:
    candidate = _strip_calendar_noise(text, remove_dates=True)
    candidate = re.sub(r"\s+", " ", candidate).strip(" .,;:!?\"'")
    if not candidate:
        return ""
    if candidate.lower() in _GENERIC_SUMMARY_WORDS:
        return ""
    return candidate


def _clean_selection_candidate(text: str) -> str:
    candidate = _LEADING_NOISE_RE.sub("", str(text or "").strip())
    candidate = _CORRECTION_PREFIX_RE.sub("", candidate)
    candidate = re.sub(r"^(?:delete|remove|cancel|update|edit|change|move|reschedule|verwijder|annuleer|wijzig|bewerk|verplaats|verzet|maak|plan)\b", "", candidate, flags=re.IGNORECASE).strip()
    candidate = re.sub(r"^(?:aan|to|for|voor)\s+", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"^(?:de|het|mijn|my)\s+", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"^(?:afs?praak|afspraken|meeting|meetings|event|events)\s+", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"\b(?:locatie|location|titel|title|beschrijving|description|attendees|deelnemers)\b.*$", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"\s+", " ", candidate).strip(" .,;:!?\"'")
    return candidate


def _score_intent(intent: AppointmentIntent) -> float:
    weights: dict[str, float] = {}
    if intent.selection_query:
        weights["selection_query"] = 0.2
    if intent.summary:
        weights["summary"] = 0.2
    if intent.start:
        weights["start"] = 0.2
    if intent.end:
        weights["end"] = 0.1
    if intent.location:
        weights["location"] = 0.05
    return score_action_intent(action=intent.action, missing_fields=intent.missing_fields, base=0.1, weights=weights)