import json
from calendar import monthrange
from collections import Counter, defaultdict
from datetime import date, timedelta

from availability.models import BlockedPeriod
from bookings.models import Booking
from django.db import models, transaction
from django.db.models import F, Func, Value
from django.db.models.functions import Cast, Coalesce
from django.utils.text import capfirst
from properties.models import Property
from rest_framework.exceptions import ValidationError
from structures.models import Structure

from guests.models import Guest
from guests.guest_defaults import (
    ISTAT_NOT_SPECIFIED,
    ISTAT_NOT_SPECIFIED_DISPLAY,
    normalize_guest_for_istat,
)
from istat.municipalities import resolve_residence
from istat.models import (
    IstatCountry,
    IstatGuestType,
    IstatTourismType,
    IstatTransportType,
)
from services.istat_export_service import (
    TOURISM_VALUE_PAIRS,
    TRANSPORT_VALUE_PAIRS,
    _build_country_lookup,
    _build_value_map,
    _resolve_country_code,
    build_istat_preview,
    generate_istat_export,
)
from services.country_utils import (
    country_name_from_iso2,
    is_iso2_country,
    normalize_country,
)
from services.guest_night_service import generate_guest_nights, with_guest_night_prefetch


DEFAULT_GUEST_TYPE_OPTIONS = ["16", "17", "18", "19", "20"]

ISSUE_FIELD_CONFIG = {
    "arrival_date": {
        "label": "Missing Arrival Date",
        "fix_type": "manual_only",
    },
    "birth_date": {
        "label": "Missing Birth Date",
        "fix_type": "manual_only",
    },
    "country": {
        "label": "Missing Residence Country",
        "fix_type": "select",
    },
    "country_of_birth": {
        "label": "Missing Country of Birth",
        "fix_type": "select",
    },
    "gender": {
        "label": "Missing Gender",
        "fix_type": "select",
    },
    "guest_type": {
        "label": "Missing Guest Type",
        "fix_type": "select",
    },
    "nationality": {
        "label": "Missing Nationality",
        "fix_type": "select",
    },
    "position_code": {
        "label": "Missing Position Code",
        "fix_type": "manual_only",
    },
    "residence_municipality": {
        "label": "Missing Residence Municipality",
        "fix_type": "manual_only",
    },
    "residence_province": {
        "label": "Missing Residence Province",
        "fix_type": "manual_only",
    },
    "tourism_type": {
        "label": "Missing Tourism Type",
        "fix_type": "select",
    },
    "transport_type": {
        "label": "Missing Transport Type",
        "fix_type": "select",
    },
}

AUTO_FIX_FIELD_CONFIG = {
    "gender": {
        "model_field": "gender",
        "extra_data_key": "gender",
    },
    "country_of_birth": {
        "model_field": "country_of_birth",
    },
    "nationality": {
        "model_field": "nationality",
        "extra_data_key": "nationality",
    },
    "country": {
        "model_field": "country",
        "extra_data_key": "country",
    },
    "guest_type": {
        "model_field": "guest_type",
    },
    "tourism_type": {
        "model_field": "tourism_type",
    },
    "transport_type": {
        "model_field": "transport_type",
    },
}

BULK_FIXABLE_FIELDS = frozenset(AUTO_FIX_FIELD_CONFIG.keys())


def get_period_date_range(period):
    start_date = date(period["year"], period["from_month"], 1)
    end_date = date(
        period["year"],
        period["to_month"],
        monthrange(period["year"], period["to_month"])[1],
    )
    return start_date, end_date


def generate_preview_for_date_range(*, structure_id, start_date, end_date):
    return build_istat_preview(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
    )


def generate_preview_for_period(*, structure_id, period):
    start_date, end_date = get_period_date_range(period)
    return generate_preview_for_date_range(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
    )


def generate_export_for_date_range(*, structure_id, start_date, end_date):
    return generate_istat_export(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
    )


def generate_export_for_period(*, structure_id, period):
    start_date, end_date = get_period_date_range(period)
    return generate_export_for_date_range(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
    )


def build_daily_calendar_for_date_range(*, structure_id, start_date, end_date):
    structure = Structure.objects.filter(id=structure_id).first()
    if not structure:
        raise ValueError("Structure not found.")

    sellable_properties = list(
        Property.objects.filter(
            structure=structure,
            availability=Property.Availability.AVAILABLE,
        )
        .select_related("property_type")
        .order_by("id")
    )
    sellable_property_ids = [prop.id for prop in sellable_properties]
    beds_by_property_id = {
        prop.id: max(int(getattr(prop.property_type, "num_beds", 0) or 0), 0)
        for prop in sellable_properties
    }
    total_rooms = len(sellable_properties)
    total_beds = sum(beds_by_property_id.values())

    day_step = timedelta(days=1)
    end_date_exclusive = end_date + day_step
    days_index = {}
    cursor = start_date
    while cursor <= end_date:
        days_index[cursor] = {
            "blocked_properties": set(),
            "occupied_properties": set(),
            "occupied_guests_by_property": defaultdict(int),
            "arrivals": 0,
            "departures": 0,
            "presences": 0,
        }
        cursor += day_step

    if sellable_property_ids:
        blocked_periods = BlockedPeriod.objects.filter(
            structure=structure,
            property_id__in=sellable_property_ids,
            start_date__lte=end_date,
            end_date__gt=start_date,
        ).values_list("property_id", "start_date", "end_date")

        for property_id, block_start, block_end in blocked_periods:
            cursor = max(block_start, start_date)
            final = min(block_end, end_date_exclusive)
            while cursor < final:
                days_index[cursor]["blocked_properties"].add(property_id)
                cursor += day_step

    bookings = list(
        with_guest_night_prefetch(
            Booking.objects.filter(
                structure=structure,
                check_in_date__lte=end_date,
                check_out_date__gt=start_date,
            )
        ).order_by("check_in_date", "id")
    )

    guest_nights = generate_guest_nights(
        bookings,
        period_start=start_date,
        period_end_exclusive=end_date_exclusive,
    )

    for guest_night in guest_nights:
        day_payload = days_index[guest_night.date]
        day_payload["presences"] += 1
        if guest_night.is_arrival_night:
            day_payload["arrivals"] += 1
        if guest_night.is_departure_night:
            day_payload["departures"] += 1

        if guest_night.property_id:
            day_payload["occupied_properties"].add(guest_night.property_id)
            day_payload["occupied_guests_by_property"][guest_night.property_id] += 1

    months = []
    month_cursor = date(start_date.year, start_date.month, 1)
    while month_cursor <= end_date:
        month_start = max(month_cursor, start_date)
        month_end = min(
            date(
                month_cursor.year,
                month_cursor.month,
                monthrange(month_cursor.year, month_cursor.month)[1],
            ),
            end_date,
        )

        days = []
        cursor = month_start
        while cursor <= month_end:
            day_payload = days_index[cursor]
            blocked_properties = day_payload["blocked_properties"]
            occupied_guests_by_property = day_payload["occupied_guests_by_property"]

            available_beds = 0
            for property_id in sellable_property_ids:
                if property_id in blocked_properties:
                    continue

                remaining_beds = (
                    beds_by_property_id[property_id]
                    - occupied_guests_by_property.get(property_id, 0)
                )
                if remaining_beds > 0:
                    available_beds += remaining_beds

            days.append(
                {
                    "date": cursor,
                    "day": cursor.day,
                    "available_rooms": max(total_rooms - len(blocked_properties), 0),
                    "available_beds": available_beds,
                    "occupied_rooms": len(day_payload["occupied_properties"]),
                    "arrivals": day_payload["arrivals"],
                    "departures": day_payload["departures"],
                    "presences": day_payload["presences"],
                    "status": "open",
                }
            )
            cursor += day_step

        months.append(
            {
                "year": month_cursor.year,
                "month": month_cursor.month,
                "label": month_cursor.strftime("%B %Y"),
                "total_rooms": total_rooms,
                "total_beds": total_beds,
                "days": days,
            }
        )

        if month_cursor.month == 12:
            month_cursor = date(month_cursor.year + 1, 1, 1)
        else:
            month_cursor = date(month_cursor.year, month_cursor.month + 1, 1)

    return {
        "structure_id": structure.id,
        "structure_name": structure.name,
        "period": {
            "year": start_date.year,
            "from_month": start_date.month,
            "to_month": end_date.month,
        },
        "months": months,
    }


def build_daily_calendar_for_period(*, structure_id, period):
    start_date, end_date = get_period_date_range(period)
    return build_daily_calendar_for_date_range(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
    )


def build_issues_summary(*, structure_id, period):
    preview = generate_preview_for_period(structure_id=structure_id, period=period)
    grouped_issue_ids = _group_issue_guest_ids(preview["invalid_records"])
    if not grouped_issue_ids:
        return {"issues": []}

    lookup_bundle = _build_fix_lookup_bundle()
    valid_guest_ids = [
        record["guest_id"]
        for record in preview["record_payloads"]
        if record.get("status") == "valid"
    ]
    suggested_values = _build_suggested_values(
        structure_id=structure_id,
        period=period,
        valid_guest_ids=valid_guest_ids,
        fields=set(grouped_issue_ids).intersection(BULK_FIXABLE_FIELDS),
        lookup_bundle=lookup_bundle,
    )

    issues = []
    for field, guest_ids in sorted(
        grouped_issue_ids.items(),
        key=lambda item: (-len(item[1]), _issue_label(item[0])),
    ):
        config = ISSUE_FIELD_CONFIG.get(field, {})
        fix_type = config.get("fix_type", "manual_only")
        issue = {
            "field": field,
            "label": config.get("label", _default_issue_label(field)),
            "count": len(guest_ids),
            "guest_ids": sorted(guest_ids),
            "fix_type": fix_type,
        }
        if fix_type == "select":
            issue["options"] = _options_for_field(field, lookup_bundle)
            issue["suggested"] = suggested_values.get(field)
        issues.append(issue)

    return {"issues": issues}


def apply_bulk_issue_fixes(*, structure_id, period, fixes):
    preview = generate_preview_for_period(structure_id=structure_id, period=period)
    grouped_issue_ids = _group_issue_guest_ids(preview["invalid_records"])
    lookup_bundle = _build_fix_lookup_bundle()

    normalized_fixes = []
    updated_guest_ids = set()
    for fix in fixes:
        field = fix["field"]
        if field not in BULK_FIXABLE_FIELDS:
            raise ValidationError(
                {"fixes": [f"Field '{field}' does not support bulk fixes."]}
            )

        requested_guest_ids = set(fix["guest_ids"])
        eligible_guest_ids = grouped_issue_ids.get(field, set())
        invalid_guest_ids = sorted(requested_guest_ids - eligible_guest_ids)
        if invalid_guest_ids:
            raise ValidationError(
                {
                    "fixes": [
                        (
                            f"Guest IDs {invalid_guest_ids} do not currently "
                            f"have the '{field}' issue in the selected period."
                        )
                    ]
                }
            )

        canonical_value = _normalize_fix_value(
            field=field,
            raw_value=fix["value"],
            lookup_bundle=lookup_bundle,
        )
        normalized_fixes.append(
            {
                "field": field,
                "value": canonical_value,
                "guest_ids": sorted(requested_guest_ids),
            }
        )
        updated_guest_ids.update(requested_guest_ids)

    start_date, end_date = get_period_date_range(period)
    with transaction.atomic():
        for fix in normalized_fixes:
            _apply_bulk_fix(
                structure_id=structure_id,
                start_date=start_date,
                end_date=end_date,
                field=fix["field"],
                canonical_value=fix["value"],
                guest_ids=fix["guest_ids"],
            )

    refreshed_preview = generate_preview_for_period(
        structure_id=structure_id,
        period=period,
    )
    return {
        "success": True,
        "updated_count": len(updated_guest_ids),
        "remaining_issues": len(refreshed_preview["invalid_records"]),
    }


def _build_suggested_values(*, structure_id, period, valid_guest_ids, fields, lookup_bundle):
    if not valid_guest_ids or not fields:
        return {}

    start_date, end_date = get_period_date_range(period)
    counters = {field: Counter() for field in fields}
    queryset = _guest_queryset(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
        guest_ids=valid_guest_ids,
    ).select_related("booking")

    for guest in queryset:
        for field in fields:
            value = _effective_field_value(
                guest=guest,
                field=field,
                lookup_bundle=lookup_bundle,
            )
            if value:
                counters[field][value] += 1

    return {
        field: counter.most_common(1)[0][0]
        for field, counter in counters.items()
        if counter
    }


def _group_issue_guest_ids(invalid_records):
    grouped = defaultdict(set)
    for record in invalid_records:
        guest_id = record.get("guest_id")
        for field in record.get("errors", []):
            grouped[field].add(guest_id)
    return grouped


def _build_fix_lookup_bundle():
    country_bundle = _build_country_bundle()
    tourism_bundle = _build_named_option_bundle(
        model=IstatTourismType,
        alias_pairs=TOURISM_VALUE_PAIRS,
    )
    transport_bundle = _build_named_option_bundle(
        model=IstatTransportType,
        alias_pairs=TRANSPORT_VALUE_PAIRS,
    )
    guest_type_options = list(
        IstatGuestType.objects.order_by("code").values_list("code", flat=True)
    )
    if not guest_type_options:
        guest_type_options = DEFAULT_GUEST_TYPE_OPTIONS.copy()

    return {
        "country": country_bundle,
        "tourism_type": tourism_bundle,
        "transport_type": transport_bundle,
        "guest_type_options": guest_type_options,
        "guest_type_set": set(guest_type_options),
    }


def _build_country_bundle():
    lookups = _build_country_lookup()
    preferred_by_code = {}
    options = []
    for country in IstatCountry.objects.order_by("name").only(
        "code", "iso_code", "name"
    ):
        option = normalize_country(country.iso_code or country.code)
        if not option or not is_iso2_country(option):
            continue
        options.append(option)
        if country.code:
            preferred_by_code[_normalize(country.code)] = option
        if country.name:
            preferred_by_code[_normalize(country.name)] = option
        if country.iso_code:
            preferred_by_code[_normalize(country.iso_code)] = option

    return {
        "lookups": lookups,
        "preferred_by_code": preferred_by_code,
        "options": list(dict.fromkeys(options)),
    }


def _build_named_option_bundle(*, model, alias_pairs):
    options = list(model.objects.order_by("name").values_list("name", flat=True))
    normalized_index = {_normalize(option): option for option in options}
    alias_index = {}
    not_specified = normalized_index.get(_normalize(ISTAT_NOT_SPECIFIED_DISPLAY))
    if not_specified:
        alias_index[_normalize(ISTAT_NOT_SPECIFIED)] = not_specified
    if alias_pairs:
        translated_values = _build_value_map(alias_pairs)
        for raw_value, translated_value in translated_values.items():
            canonical = normalized_index.get(_normalize(translated_value))
            if canonical:
                alias_index[raw_value] = canonical

    return {
        "index": normalized_index,
        "alias_index": alias_index,
        "options": options,
    }


def _effective_field_value(*, guest, field, lookup_bundle):
    guest = normalize_guest_for_istat(guest)
    extra_data = _coerce_extra_data(guest.extra_data)

    if field == "residence_municipality":
        residence = resolve_residence(
            country=extra_data.get("country") or guest.country,
            city=guest.city,
            province=extra_data.get("province"),
            region=guest.region,
            extra_data=extra_data,
        )
        return residence.municipality_name
    if field == "residence_province":
        residence = resolve_residence(
            country=extra_data.get("country") or guest.country,
            city=guest.city,
            province=extra_data.get("province"),
            region=guest.region,
            extra_data=extra_data,
        )
        return residence.province_code if residence.province_matches else None
    if field == "gender":
        return _canonical_gender_option(extra_data.get("gender") or guest.gender)
    if field == "country_of_birth":
        return _display_country_option(
            guest.country_of_birth
            or extra_data.get("country_of_birth")
            or extra_data.get("place_of_birth"),
            lookup_bundle["country"],
        )
    if field == "nationality":
        return _display_country_option(
            extra_data.get("nationality") or guest.nationality,
            lookup_bundle["country"],
        )
    if field == "country":
        return _display_country_option(
            extra_data.get("country") or guest.country,
            lookup_bundle["country"],
        )
    if field == "guest_type":
        raw_value = guest.guest_type or extra_data.get("guest_type")
        return _canonical_guest_type_option(raw_value, lookup_bundle)
    if field == "tourism_type":
        return _canonical_named_option(
            guest.tourism_type
            or extra_data.get("tourism_type")
            or getattr(guest.booking, "tourism_type", None),
            lookup_bundle["tourism_type"],
        )
    if field == "transport_type":
        return _canonical_named_option(
            guest.transport_type
            or extra_data.get("transport_type")
            or getattr(guest.booking, "transport_type", None),
            lookup_bundle["transport_type"],
        )
    return None


def _normalize_fix_value(*, field, raw_value, lookup_bundle):
    if field == "gender":
        canonical_value = _canonical_gender_option(raw_value)
        if not canonical_value:
            raise ValidationError(
                {"fixes": ["Gender fixes must use 'M' or 'F'."]}
            )
        return canonical_value

    if field in {"country_of_birth", "nationality", "country"}:
        canonical_value = _canonical_country_storage_value(
            raw_value,
            lookup_bundle["country"],
        )
        if not canonical_value:
            raise ValidationError(
                {
                    "fixes": [
                        f"'{raw_value}' is not a valid country option for '{field}'."
                    ]
                }
            )
        return canonical_value

    if field == "guest_type":
        canonical_value = _canonical_guest_type_option(raw_value, lookup_bundle)
        if not canonical_value:
            raise ValidationError(
                {
                    "fixes": [
                        f"'{raw_value}' is not a valid guest type for bulk fixing."
                    ]
                }
            )
        return canonical_value

    if field in {"tourism_type", "transport_type"}:
        canonical_value = _canonical_named_option(
            raw_value,
            lookup_bundle[field],
        )
        if not canonical_value:
            raise ValidationError(
                {
                    "fixes": [
                        f"'{raw_value}' is not a valid option for '{field}'."
                    ]
                }
            )
        return canonical_value

    raise ValidationError(
        {"fixes": [f"Field '{field}' does not support bulk fixes."]}
    )


def _apply_bulk_fix(*, structure_id, start_date, end_date, field, canonical_value, guest_ids):
    config = AUTO_FIX_FIELD_CONFIG[field]
    stored_value = _storage_value_for_field(field, canonical_value)
    queryset = _guest_queryset(
        structure_id=structure_id,
        start_date=start_date,
        end_date=end_date,
        guest_ids=guest_ids,
    )

    update_kwargs = {config["model_field"]: stored_value}
    extra_data_key = config.get("extra_data_key")
    if extra_data_key:
        update_kwargs["extra_data"] = _jsonb_set_extra_data_value(
            key=extra_data_key,
            value=stored_value,
        )

    updated_rows = queryset.update(**update_kwargs)
    if updated_rows != len(guest_ids):
        raise ValidationError(
            {
                "fixes": [
                    (
                        "One or more guests could not be updated for field "
                        f"'{field}'."
                    )
                ]
            }
        )


def _storage_value_for_field(field, canonical_value):
    if field == "gender":
        return {"M": "male", "F": "female"}[canonical_value]
    return canonical_value


def _guest_queryset(*, structure_id, start_date, end_date, guest_ids=None):
    queryset = Guest.objects.filter(
        booking__structure_id=structure_id,
        booking__is_checked_in=True,
        booking__check_in_date__gte=start_date,
        booking__check_out_date__lte=end_date,
    )
    if guest_ids is not None:
        queryset = queryset.filter(id__in=guest_ids)
    return queryset


def _jsonb_set_extra_data_value(*, key, value):
    return Func(
        Coalesce(F("extra_data"), Value({}, output_field=models.JSONField())),
        Value(f"{{{key}}}"),
        Cast(Value(json.dumps(value)), models.JSONField()),
        Value(True),
        function="jsonb_set",
        output_field=models.JSONField(),
    )


def _options_for_field(field, lookup_bundle):
    if field == "gender":
        return ["M", "F"]
    if field in {"country_of_birth", "nationality", "country"}:
        return lookup_bundle["country"]["options"]
    if field == "guest_type":
        return lookup_bundle["guest_type_options"]
    if field in {"tourism_type", "transport_type"}:
        return lookup_bundle[field]["options"]
    return []


def _canonical_gender_option(value):
    normalized = _normalize(value)
    if normalized in {"m", "male", "maschio", "1"}:
        return "M"
    if normalized in {"f", "female", "femmina", "2"}:
        return "F"
    return None


def _display_country_option(value, country_bundle):
    normalized = normalize_country(value)
    if not normalized or not is_iso2_country(normalized):
        return None
    return normalized


def _canonical_country_storage_value(value, country_bundle):
    normalized = normalize_country(value)
    if normalized and is_iso2_country(normalized):
        return normalized
    return None


def _canonical_guest_type_option(value, lookup_bundle):
    raw_value = str(value or "").strip()
    if not raw_value:
        return None
    if raw_value in lookup_bundle["guest_type_set"]:
        return raw_value
    return None


def _canonical_named_option(value, option_bundle):
    normalized = _normalize(value)
    if not normalized:
        return None
    if normalized in option_bundle["index"]:
        return option_bundle["index"][normalized]
    return option_bundle["alias_index"].get(normalized)


def _coerce_extra_data(value):
    if isinstance(value, dict):
        return value
    return {}


def _first_present(*values):
    for value in values:
        if value is None:
            continue
        normalized = str(value).strip()
        if normalized:
            return normalized
    return None


def _effective_booking_guest_count(booking):
    booking_count = max(
        int(booking.adults_count or 0) + int(booking.children_count or 0),
        0,
    )
    actual_guest_count = len(booking.guests.all())

    if booking_count <= 0:
        return actual_guest_count
    if actual_guest_count < booking_count:
        return booking_count
    return actual_guest_count


def _normalize(value):
    if value is None:
        return ""
    return str(value).strip().lower()


def _issue_label(field):
    return ISSUE_FIELD_CONFIG.get(field, {}).get("label", _default_issue_label(field))


def _default_issue_label(field):
    return f"Missing {capfirst(field.replace('_', ' '))}"
