from datetime import date as dt_date, datetime, timedelta
from typing import Dict, List, Tuple

from django.utils import timezone as dj_timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication
from rest_framework_simplejwt.authentication import JWTAuthentication
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, OpenApiExample, inline_serializer
from rest_framework import serializers

from bazi.models import Person
from iching.utils import bz as bzu
from iching.utils.person_good_days import get_month_reasons_cached
from iching.utils import bzshagod as sg

# Build a static doc block for Sha God index mapping so it shows up in /api/docs
_SHAGOD_DOC_LINES = ", ".join([f"{k}: {v}" for k, v in sg.gShagodNames.items()])

_BASE_DESCRIPTION = """
Return compact good-day flags for a date range.

Compact response schema:
- user: { person_id?, day_stem, day_branch, source }
- range: { start, end, timezone }
- days: [ { date, day:{g,e}, month:{e}, is_good, reasons } ]
- shagod: { index -> short Chinese name }

reasons entries are compact:
- Relationship: { t: 'r', c: <int>, by: [[who,pillar,component,index], ...] }
  where c indices are:
    0: 六合, 1: 半三合, 3: 六冲, 6: 三合 (full triad)
    Note: 六冲 (index 3) is a clash relationship displayed as black badges in the UI
- Sha god:     { t: 's', i: <int>, by: [[who,pillar,component,index], ...] }

Abbreviations: who = 'u' (user) | 'd' (date); pillar = 'd' (day) | 'm' (month);
component = 'g' (stem) | 'e' (branch); index = stem/branch index.

Sha God index mapping (i -> name):
"""

_GOOD_DAYS_DESCRIPTION = _BASE_DESCRIPTION + "\n" + _SHAGOD_DOC_LINES


def _month_start_end(now_tz) -> Tuple[dt_date, dt_date]:
    # Default to current month in gMYTimezone
    # Use bzu.gMYTimezone per project convention
    today_tz = datetime.now(bzu.gMYTimezone).date()
    start = dt_date(today_tz.year, today_tz.month, 1)
    if start.month == 12:
        next_first = dt_date(start.year + 1, 1, 1)
    else:
        next_first = dt_date(start.year, start.month + 1, 1)
    end = next_first - timedelta(days=1)
    return start, end


def _parse_date(s: str) -> dt_date | None:
    try:
        return datetime.strptime(s, "%Y-%m-%d").date()
    except Exception:
        return None


def _month_iter(start: dt_date, end: dt_date):
    y, m = start.year, start.month
    while True:
        first = dt_date(y, m, 1)
        if first > end:
            break
        yield first
        if m == 12:
            y += 1
            m = 1
        else:
            m += 1


class GoodDaysAPIView(APIView):
    authentication_classes = [SessionAuthentication, JWTAuthentication]
    permission_classes = [IsAuthenticated]

    @extend_schema(
        description=_GOOD_DAYS_DESCRIPTION,
        parameters=[
            OpenApiParameter(name='start', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False, description='Start date YYYY-MM-DD (defaults to 1st of current month, UTC+8)'),
            OpenApiParameter(name='end', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False, description='End date YYYY-MM-DD (defaults to last day of current month, UTC+8)'),
            OpenApiParameter(name='person_id', type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False, description='Use a specific Person owned by the user. If omitted, uses owner=True record.'),
            OpenApiParameter(name='dob', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False, description='Optional. Use provided birth date (YYYY-MM-DD) instead of Person to compute BaZi.'),
            OpenApiParameter(name='time', type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False, description='Optional. HH:MM in 24h (UTC+8). Used with dob to determine day pillar boundary (23:00 crossover).'),
        ],
        examples=[
            OpenApiExample(
                'Example response',
                value={
                    "user": {"person_id": 123, "day_stem": 0, "day_branch": 2, "source": "person"},
                    "range": {"start": "2025-08-01", "end": "2025-08-31", "timezone": "+08:00"},
                    "days": [
                        {
                            "date": "2025-08-03",
                            "day": {"g": 4, "e": 6},
                            "month": {"e": 7},
                            "is_good": True,
                            "reasons": [
                                {"t": "r", "c": 0, "by": [["u","d","e",2],["d","d","e",6]]},
                                {"t": "r", "c": 3, "by": [["u","d","e",2],["d","d","e",6]]},
                                {"t": "s", "i": 5, "by": [["u","d","g",0],["d","d","e",6]]}
                            ]
                        }
                    ]
                }
            )
        ],
        responses=inline_serializer(
            name='GoodDaysResponse',
            fields={
                'user': inline_serializer(
                    name='GoodDaysUser',
                    fields={
                        'person_id': serializers.IntegerField(required=False, allow_null=True),
                        'day_stem': serializers.IntegerField(),
                        'day_branch': serializers.IntegerField(),
                        'source': serializers.ChoiceField(choices=['person', 'params'])
                    }
                ),
                'range': inline_serializer(
                    name='GoodDaysRange',
                    fields={
                        'start': serializers.DateField(),
                        'end': serializers.DateField(),
                        'timezone': serializers.CharField()
                    }
                ),
                'days': serializers.ListField(child=inline_serializer(
                    name='GoodDayItem',
                    fields={
                        'date': serializers.DateField(),
                        'day': inline_serializer(name='PillarDay', fields={'g': serializers.IntegerField(), 'e': serializers.IntegerField()}),
                        'month': inline_serializer(name='PillarMonth', fields={'e': serializers.IntegerField()}),
                        'is_good': serializers.BooleanField(),
                        'reasons': serializers.ListField(child=serializers.DictField(), help_text='Compact reasons as documented above')
                    }
                )),
                'shagod': serializers.DictField(child=serializers.CharField(), help_text='Mapping: sha-god index -> short Chinese name')
            }
        )
    )
    def get(self, request):
        # Date range
        start_param = request.query_params.get("start")
        end_param = request.query_params.get("end")

        if start_param and end_param:
            start = _parse_date(start_param)
            end = _parse_date(end_param)
            if not start or not end or end < start:
                return Response({"error": "Invalid date range"}, status=400)
        else:
            start, end = _month_start_end(bzu.gMYTimezone)

        # Source selection: params (dob/time) OR person
        user_info = {"person_id": None, "day_stem": None, "day_branch": None, "source": None}
        dob_param = request.query_params.get("dob")
        time_param = request.query_params.get("time")

        if dob_param:
            # Use provided birth date/time (UTC+8) to compute user's day pillar
            try:
                dob_date = datetime.strptime(dob_param, "%Y-%m-%d")
                if time_param:
                    hh, mm = [int(x) for x in time_param.split(":")]
                else:
                    hh, mm = 0, 0
                # Convert to UTC+8 and compute pillars
                dt_local = datetime(dob_date.year, dob_date.month, dob_date.day, hh, mm, 0, tzinfo=bzu.gMYTimezone)
                # Use calendar10k util to get indices (uses standard crossover rules)
                b = bzu.getDateTimeGodEarthStem(dt_local.year, dt_local.month, dt_local.day, dt_local.hour, dt_local.minute, dt_local.second)
                user_day_g = b["day"]["g"]
                user_day_e = b["day"]["e"]
                user_info.update({"day_stem": user_day_g, "day_branch": user_day_e, "source": "params"})
            except Exception:
                return Response({"error": "Invalid dob/time format. dob=YYYY-MM-DD, time=HH:MM (24h)."}, status=400)
        else:
            # Person selection
            person_id = request.query_params.get("person_id")
            person = None
            if person_id:
                try:
                    person = Person.objects.get(pk=int(person_id), created_by=request.user)
                except (Person.DoesNotExist, ValueError):
                    person = None
            if person is None:
                person = (
                    Person.objects.filter(created_by=request.user, owner=True)
                    .order_by("-created_at")
                    .first()
                )

            if person is None or not person.bazi_result or "day" not in person.bazi_result:
                return Response({"user": None, "days": []})

            try:
                user_day_g = person.bazi_result["day"]["god"]
                user_day_e = person.bazi_result["day"]["earth"]
            except Exception:
                return Response({"user": None, "days": []})
            user_info.update({"person_id": person.id, "day_stem": user_day_g, "day_branch": user_day_e, "source": "person"})

        # Iterate months, use cached monthly results
        results: List[Dict] = []
        current = start
        months = list(_month_iter(start, end))

        # Cache monthly maps: month_str -> (branch_map, stem_map)
        monthly_cache: Dict[str, Tuple[Dict[str, List[Dict]], Dict[str, List[Dict]]]] = {}

        for month_first in months:
            month_str = f"{month_first.year:04d}{month_first.month:02d}"
            branch_map, stem_map = get_month_reasons_cached(
                user_day_g, user_day_e, month_first, ttl_seconds=None
            )
            monthly_cache[month_str] = (branch_map, stem_map)

        # Walk each day and assemble response
        day = start
        while day <= end:
            month_str = f"{day.year:04d}{day.month:02d}"
            branch_map, stem_map = monthly_cache.get(month_str, ({}, {}))
            key = day.isoformat()
            reasons = (branch_map.get(key) or []) + (stem_map.get(key) or [])

            # Always compute day/month pillars for output consistency
            bazi = bzu.getCalendar10kGodEarthStem(day.year, day.month, day.day, 22, 59, 59)
            results.append(
                {
                    "date": key,
                    "day": {"g": bazi["day"]["g"], "e": bazi["day"]["e"]},
                    "month": {"e": bazi["month"]["e"]},
                    "is_good": bool(reasons),
                    "reasons": reasons,
                }
            )

            day += timedelta(days=1)

        # Also include a mapping of sha-god indices for client interpretation
        shagod_index_map = sg.gShagodNames  # id -> short Chinese name
        return Response(
            {
                "user": user_info,
                "range": {
                    "start": start.isoformat(),
                    "end": end.isoformat(),
                    "timezone": "+08:00",
                },
                "days": results,
                "shagod": shagod_index_map,
            }
        )


