from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from datetime import date
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

from src.errors import UserError

REQUIRED_MAPPING_FIELDS = ("event_date", "venue_name", "lat", "lon")


@dataclass(frozen=True)
class ResolvedMapping:
    schema_version: int
    fields: Dict[str, str]

    def to_json(self) -> str:
        ordered = {
            "schema_version": self.schema_version,
            "fields": dict(sorted(self.fields.items())),
        }
        return json.dumps(ordered, ensure_ascii=True, sort_keys=True)


@dataclass(frozen=True)
class NormalizedDeal:
    deal_id: int
    title: str
    event_date: date
    venue_name: str
    lat: float
    lon: float
    place: str | None = None
    region: str | None = None

    def to_json(self) -> str:
        payload = asdict(self)
        payload["event_date"] = self.event_date.isoformat()
        return json.dumps(payload, ensure_ascii=True, sort_keys=True)


def load_mapping_file(path: Path) -> Dict[str, Any]:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise UserError(f"Invalid JSON in mapping file: {exc.msg}") from exc
    if not isinstance(data, dict):
        raise UserError("Mapping file must contain a JSON object.")
    return data


def validate_mapping_spec(data: Dict[str, Any]) -> None:
    if data.get("schema_version") != 1:
        raise UserError("Mapping schema_version must be 1.")
    fields = data.get("fields")
    if not isinstance(fields, dict):
        raise UserError("Mapping must contain object 'fields'.")

    for key in REQUIRED_MAPPING_FIELDS:
        if key not in fields:
            raise UserError(f"Missing required mapping field: {key}")
        _validate_selector(fields[key], key)


def resolve_mapping(data: Dict[str, Any], deal_fields: List[Dict[str, Any]]) -> ResolvedMapping:
    validate_mapping_spec(data)
    fields_spec = data["fields"]
    available = _index_deal_fields(deal_fields)
    resolved: Dict[str, str] = {}

    for out_key, selector in fields_spec.items():
        resolved[out_key] = _resolve_selector(out_key, selector, available)

    return ResolvedMapping(schema_version=1, fields=resolved)


def normalize_deals_in_range(
    deals: Iterable[Dict[str, Any]],
    mapping: ResolvedMapping,
    *,
    from_date: date,
    to_date: date,
) -> List[NormalizedDeal]:
    required = [mapping.fields[k] for k in REQUIRED_MAPPING_FIELDS]
    place_key = mapping.fields.get("place")
    region_key = mapping.fields.get("region")
    out: List[NormalizedDeal] = []
    for deal in deals:
        deal_id = _require_int(deal.get("id"), "id")
        title = str(deal.get("title") or "")

        event_raw = _require_value(deal, required[0], deal_id)
        event_date = _parse_date(event_raw, deal_id, required[0])
        if event_date < from_date or event_date > to_date:
            continue

        venue = str(_require_value(deal, required[1], deal_id) or "")
        lat = _parse_float(_require_value(deal, required[2], deal_id), deal_id, required[2])
        lon = _parse_float(_require_value(deal, required[3], deal_id), deal_id, required[3])
        out.append(
            NormalizedDeal(
                deal_id=deal_id,
                title=title,
                event_date=event_date,
                venue_name=venue,
                lat=lat,
                lon=lon,
                place=_optional_str(deal, place_key),
                region=_optional_str(deal, region_key),
            )
        )
    return out


def _optional_str(deal: Dict[str, Any], key: str | None) -> str | None:
    if not key:
        return None
    value = deal.get(key)
    if value is None:
        return None
    text = str(value).strip()
    return text if text else None


def _validate_selector(selector: Any, out_key: str) -> None:
    if not isinstance(selector, dict):
        raise UserError(f"Mapping field '{out_key}' must be an object.")
    field_key = selector.get("field_key")
    field_name = selector.get("field_name")
    if bool(field_key) == bool(field_name):
        raise UserError(
            f"Mapping field '{out_key}' must set exactly one of field_key or field_name."
        )
    if field_key is not None and (not isinstance(field_key, str) or not field_key.strip()):
        raise UserError(f"Mapping field '{out_key}.field_key' must be a non-empty string.")
    if field_name is not None and (not isinstance(field_name, str) or not field_name.strip()):
        raise UserError(f"Mapping field '{out_key}.field_name' must be a non-empty string.")


def _index_deal_fields(deal_fields: List[Dict[str, Any]]) -> List[Tuple[str, str]]:
    out: List[Tuple[str, str]] = []
    for f in deal_fields:
        if not isinstance(f, dict):
            continue
        key = f.get("key")
        name = f.get("name")
        if isinstance(key, str) and isinstance(name, str):
            out.append((key, name))
    if not out:
        raise UserError("No deal fields returned by Pipedrive; cannot validate mapping.")
    return out


def _resolve_selector(
    out_key: str, selector: Dict[str, Any], available: List[Tuple[str, str]]
) -> str:
    field_key = selector.get("field_key")
    field_name = selector.get("field_name")
    if isinstance(field_key, str) and field_key.strip():
        if any(k == field_key for k, _ in available):
            return field_key
        raise UserError(f"Unknown Pipedrive field key for '{out_key}': {field_key!r}")

    if not isinstance(field_name, str) or not field_name.strip():
        raise UserError(f"Invalid mapping field '{out_key}': missing field_name.")

    wanted = _norm(field_name)
    matches = [(k, n) for k, n in available if _norm(n) == wanted]
    if not matches:
        raise UserError(f"No Pipedrive field named {field_name!r} for '{out_key}'.")
    if len(matches) > 1:
        options = ", ".join([f"{k}:{n}" for k, n in matches[:8]])
        raise UserError(f"Ambiguous Pipedrive field name {field_name!r} for '{out_key}': {options}")
    return matches[0][0]


def _norm(s: str) -> str:
    return " ".join(s.strip().lower().split())


def _require_value(deal: Dict[str, Any], key: str, deal_id: int) -> Any:
    if key not in deal:
        raise UserError(f"Deal {deal_id} missing mapped field: {key}")
    return deal.get(key)


def _require_int(value: Any, name: str) -> int:
    if isinstance(value, int):
        return value
    if isinstance(value, str) and value.isdigit():
        return int(value)
    raise UserError(f"Expected integer for {name}.")


def _parse_date(value: Any, deal_id: int, key: str) -> date:
    if not isinstance(value, str) or not value.strip():
        raise UserError(f"Deal {deal_id} field {key} is not a date string.")
    raw = value.strip()
    try:
        return date.fromisoformat(raw[:10])
    except ValueError as exc:
        raise UserError(f"Deal {deal_id} field {key} is not ISO date: {raw!r}") from exc


def _parse_float(value: Any, deal_id: int, key: str) -> float:
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str) and value.strip():
        try:
            return float(value.strip())
        except ValueError as exc:
            raise UserError(f"Deal {deal_id} field {key} is not a float: {value!r}") from exc
    raise UserError(f"Deal {deal_id} field {key} is missing/empty.")
