# Standard Library
import calendar
import json
import logging
import urllib.request
from datetime import date, timedelta

# Django
from django.conf import settings as django_settings
from django.contrib.admin.views.decorators import staff_member_required
from django.core.cache import cache
from django.db import IntegrityError, transaction
from django.db.models import F, Sum
from django.db.models.functions import Greatest
from django.http import HttpResponse, JsonResponse
from django.template import TemplateDoesNotExist
from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone, translation
from django.utils.decorators import method_decorator
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView

# Local application / specific library imports
from .cache import VERSION_KEY
from .conf import settings
from .models import Application, DailyConsentStats, Purpose
from .utils import is_kapt_user

logger = logging.getLogger(__name__)


def _get_client_ip(request):
    """Extract client IP from request (handles X-Forwarded-For, X-Real-IP)."""
    xff = request.headers.get("x-forwarded-for")
    if xff:
        return xff.split(",")[0].strip()
    return request.headers.get("x-real-ip") or request.META.get("REMOTE_ADDR", "")


def _get_country_from_ip(ip):
    """
    Resolve country code (ISO 3166-1 alpha-2) from IP using ip-api.com.
    Returns None for localhost, private IPs, or on failure.
    """
    if not ip or ip in ("127.0.0.1", "::1", "localhost"):
        return None
    if ip.startswith(("10.", "172.", "192.168.")):
        return None
    try:
        url = f"http://ip-api.com/json/{ip}?fields=countryCode"
        with urllib.request.urlopen(url, timeout=2) as resp:
            data = json.loads(resp.read().decode())
            return data.get("countryCode")
    except Exception as e:
        logger.debug("GeoIP lookup failed for %s: %s", ip, e)
        return None


def _intcomma(value):
    """Format integer with thousand separators (non-breaking space, European style)."""
    if value is None:
        return ""
    s = str(int(value))
    if len(s) <= 3:
        return s
    result = []
    for i, c in enumerate(reversed(s)):
        if i and i % 3 == 0:
            result.append("\u00a0")
        result.append(c)
    return "".join(reversed(result))


class ConfigView(TemplateView):
    template_name = "cookie_optin/config.js"

    def get_function_suffix(self, application):
        """Return camel_case_name if app-specific JS exists, else 'default'."""
        try:
            get_template(
                f"cookie_optin/functions/{application.camel_case_name.lower()}.js"
            )
            return application.camel_case_name
        except TemplateDoesNotExist:
            return "default"

    def get_context_data(self, **kwargs):
        super().get_context_data(**kwargs)
        purposes = Purpose.objects.all().order_by("order")
        applications = Application.objects.filter(is_active=True).order_by(
            "purposes__order", "order"
        )

        custom_function_includes = set()
        for application in applications:
            application.function_suffix = self.get_function_suffix(application)
            if application.function_suffix != "default":
                custom_function_includes.add(
                    f"cookie_optin/functions/{application.camel_case_name.lower()}.js"
                )

        kwargs.update(
            purposes=purposes,
            applications=applications,
            klaro_api_url=self.request.build_absolute_uri(settings.API_PATH),
            custom_function_includes=sorted(custom_function_includes),
            privacy_url=settings.PRIVACY_URL,
        )

        return kwargs

    def get(self, request, *args, **kwargs):
        content_type = "application/javascript"
        if settings.CONFIG_CACHE:
            version = cache.get(VERSION_KEY) or 0
            cache_key = f"cookie_optin_config_{version}_{request.get_host()}"
            cached = cache.get(cache_key)
            if cached is not None:
                return HttpResponse(cached, content_type=content_type)
        context = self.get_context_data(**kwargs)
        response = self.render_to_response(context, content_type=content_type)
        if settings.CONFIG_CACHE:
            response.render()
            cache.set(
                cache_key,
                response.content,
                timeout=settings.CONFIG_CACHE_TIMEOUT,
            )
        return response


@method_decorator(csrf_exempt, name="dispatch")
@method_decorator(require_http_methods(["POST"]), name="dispatch")
class ConsentApiView(View):
    """
    API endpoint that receives consent data from Klaro frontend.
    Expects JSON body: { consent_data: { consents, type }, ... }
    """

    def post(self, request):
        try:
            data = json.loads(request.body)
        except json.JSONDecodeError as e:
            logger.warning("Invalid JSON in Klaro API request: %s", e)
            return JsonResponse(
                {"error": "Invalid JSON"},
                status=400,
            )

        # Klaro API format: { consent_data: { consents, changes, type, config, first_interaction }, ... }
        consent_data = data.get("consent_data", {})
        if consent_data:
            consents = consent_data.get("consents", {})
            raw_type = consent_data.get("type", "")
            first_interaction = consent_data.get("first_interaction", False)
            # Map Klaro types to our choice_type
            type_to_choice = {
                "accept": "accept",
                "decline": "decline",
                "save": "save",
                "contextual-accept": "accept",
                "show": "show",
                "no_interaction": "no_interaction",
            }
            choice_type = type_to_choice.get(raw_type)
        else:
            # Legacy format: { consents, confirmed }
            consents = data.get("consents", {})
            confirmed = data.get("confirmed", False)
            choice_type = "accept" if confirmed else "decline"
            first_interaction = False

        if not isinstance(consents, dict):
            return JsonResponse(
                {"error": "consents must be an object"},
                status=400,
            )

        # Update daily counters: show = +show_count +no_interaction; accept/decline/save = +field -no_interaction
        if (
            getattr(django_settings, "COOKIE_OPTIN_STORE_CONSENT_RECORDS", True)
            and choice_type is not None
        ):
            try:
                today = timezone.localdate()
                try:
                    stats, _ = DailyConsentStats.objects.get_or_create(
                        date=today,
                        defaults={
                            "accept_count": 0,
                            "decline_count": 0,
                            "save_count": 0,
                            "show_count": 0,
                            "no_interaction_count": 0,
                        },
                    )
                except IntegrityError:
                    stats = DailyConsentStats.objects.get(date=today)
                if choice_type == "show":
                    DailyConsentStats.objects.filter(pk=stats.pk).update(
                        show_count=F("show_count") + 1,
                        no_interaction_count=F("no_interaction_count") + 1,
                    )
                    # Increment country count from client IP (or "__unknown__" if geocoding fails)
                    country_code = (
                        _get_country_from_ip(_get_client_ip(request)) or "__unknown__"
                    )
                    with transaction.atomic():
                        row = DailyConsentStats.objects.select_for_update().get(
                            pk=stats.pk
                        )
                        counts = dict(row.country_counts or {})
                        counts[country_code] = counts.get(country_code, 0) + 1
                        row.country_counts = counts
                        row.save(update_fields=["country_counts"])
                else:
                    field_map = {
                        "accept": "accept_count",
                        "decline": "decline_count",
                        "save": "save_count",
                    }
                    field = field_map.get(choice_type)
                    if field:
                        update_kwargs = {field: F(field) + 1}
                        if first_interaction:
                            update_kwargs["no_interaction_count"] = Greatest(
                                F("no_interaction_count") - 1, 0
                            )
                        DailyConsentStats.objects.filter(pk=stats.pk).update(
                            **update_kwargs
                        )
            except Exception as e:
                logger.exception("Failed to update consent stats: %s", e)

        return JsonResponse({"status": "ok"})


@method_decorator(staff_member_required, name="dispatch")
class ConsentOverviewView(TemplateView):
    """
    Overview of consent metrics for the CMS toolbar modal.
    Shows accepts, declines, acceptance rate and a time-series chart.
    Displays all days of the selected month with prev/next navigation.
    When ENABLE_OVERVIEW is False, only Kapt users (see COOKIE_OPTIN_KAPT_*) can access.
    """

    template_name = "cookie_optin/consent_overview.html"

    def dispatch(self, request, *args, **kwargs):
        if not settings.ENABLE_OVERVIEW and not is_kapt_user(request.user):
            # Django
            from django.http import HttpResponseForbidden

            return HttpResponseForbidden()
        return super().dispatch(request, *args, **kwargs)

    def get_period_params(self):
        """Get view (day|month|year), year, month, day from query params."""
        now = timezone.now()
        view = self.request.GET.get("view", "month")
        if view not in ("day", "month", "year"):
            view = "month"
        year = int(self.request.GET.get("year", now.year))
        month = int(self.request.GET.get("month", now.month))
        day = int(self.request.GET.get("day", now.day))
        # Clamp month
        if month < 1:
            month, year = 12, year - 1
        elif month > 12:
            month, year = 1, year + 1
        # Clamp day
        last_day_num = calendar.monthrange(year, month)[1]
        if day < 1:
            day = 1
        elif day > last_day_num:
            day = last_day_num
        return view, year, month, day

    def _aggregate_stats(self, queryset):
        """Aggregate DailyConsentStats into totals."""
        agg = queryset.aggregate(
            accepts=Sum("accept_count"),
            declines=Sum("decline_count"),
            saves=Sum("save_count"),
            shows=Sum("show_count"),
            no_interactions=Sum("no_interaction_count"),
        )
        return {
            "accepts": agg["accepts"] or 0,
            "declines": agg["declines"] or 0,
            "saves": agg["saves"] or 0,
            "shows": agg["shows"] or 0,
            "no_interactions": agg["no_interactions"] or 0,
        }

    def get_summary_stats(self, view, year, month, day):
        """Get summary stats for the given period."""
        if view == "day":
            d = date(year, month, day)
            try:
                r = DailyConsentStats.objects.get(date=d)
                stats = {
                    "accepts": r.accept_count,
                    "declines": r.decline_count,
                    "saves": r.save_count,
                    "shows": r.show_count,
                    "no_interactions": r.no_interaction_count,
                }
            except DailyConsentStats.DoesNotExist:
                stats = {
                    "accepts": 0,
                    "declines": 0,
                    "saves": 0,
                    "shows": 0,
                    "no_interactions": 0,
                }
        elif view == "month":
            first_day = date(year, month, 1)
            last_day = date(year, month, calendar.monthrange(year, month)[1])
            qs = DailyConsentStats.objects.filter(
                date__gte=first_day, date__lte=last_day
            )
            stats = self._aggregate_stats(qs)
        else:  # year
            first_day = date(year, 1, 1)
            last_day = date(year, 12, 31)
            qs = DailyConsentStats.objects.filter(
                date__gte=first_day, date__lte=last_day
            )
            stats = self._aggregate_stats(qs)
        return stats

    def get_navigation_urls(self, view, year, month, day):
        """Build prev/next URLs and period name for the given view."""
        overview_url = reverse("cookie_optin:overview")
        base = f"{overview_url}?view={view}"

        if view == "day":
            d = date(year, month, day)
            prev_d = d - timedelta(days=1)
            next_d = d + timedelta(days=1)
            prev_url = (
                f"{base}&year={prev_d.year}&month={prev_d.month}&day={prev_d.day}"
            )
            next_url = (
                f"{base}&year={next_d.year}&month={next_d.month}&day={next_d.day}"
            )
            period_name = date_format(d, "j F Y")
        elif view == "month":
            prev_month = month - 1 if month > 1 else 12
            prev_year = year if month > 1 else year - 1
            next_month = month + 1 if month < 12 else 1
            next_year = year if month < 12 else year + 1
            prev_url = f"{base}&year={prev_year}&month={prev_month}"
            next_url = f"{base}&year={next_year}&month={next_month}"
            period_name = date_format(date(year, month, 1), "F Y")
        else:  # year
            prev_url = f"{base}&year={year - 1}"
            next_url = f"{base}&year={year + 1}"
            period_name = str(year)

        return prev_url, next_url, period_name

    def get_chart_data(self, view, year, month, day):
        """Build chart labels and series based on view (day/month/year)."""
        chart_labels = []
        chart_accepts = []
        chart_declines = []
        chart_shows = []
        chart_no_interactions = []
        chart_saves = []

        if view == "day":
            d = date(year, month, day)
            chart_labels.append(date_format(d, "j F Y"))
            try:
                r = DailyConsentStats.objects.get(date=d)
                chart_accepts.append(r.accept_count)
                chart_declines.append(r.decline_count)
                chart_shows.append(r.show_count)
                chart_no_interactions.append(r.no_interaction_count)
                chart_saves.append(r.save_count)
            except DailyConsentStats.DoesNotExist:
                chart_accepts.append(0)
                chart_declines.append(0)
                chart_shows.append(0)
                chart_no_interactions.append(0)
                chart_saves.append(0)

        elif view == "month":
            first_day = date(year, month, 1)
            last_day_num = calendar.monthrange(year, month)[1]
            last_day = date(year, month, last_day_num)
            rows = {
                r.date: r
                for r in DailyConsentStats.objects.filter(
                    date__gte=first_day, date__lte=last_day
                )
            }
            for d in range(1, last_day_num + 1):
                d_date = date(year, month, d)
                chart_labels.append(d_date.strftime("%d/%m"))
                r = rows.get(d_date)
                if r:
                    chart_accepts.append(r.accept_count)
                    chart_declines.append(r.decline_count)
                    chart_shows.append(r.show_count)
                    chart_no_interactions.append(r.no_interaction_count)
                    chart_saves.append(r.save_count)
                else:
                    chart_accepts.append(0)
                    chart_declines.append(0)
                    chart_shows.append(0)
                    chart_no_interactions.append(0)
                    chart_saves.append(0)

        else:  # year
            for m in range(1, 13):
                first_day = date(year, m, 1)
                last_day = date(year, m, calendar.monthrange(year, m)[1])
                agg = DailyConsentStats.objects.filter(
                    date__gte=first_day, date__lte=last_day
                ).aggregate(
                    accepts=Sum("accept_count"),
                    declines=Sum("decline_count"),
                    saves=Sum("save_count"),
                    shows=Sum("show_count"),
                    no_interactions=Sum("no_interaction_count"),
                )
                chart_labels.append(date_format(first_day, "M"))  # abbreviated month
                chart_accepts.append(agg["accepts"] or 0)
                chart_declines.append(agg["declines"] or 0)
                chart_shows.append(agg["shows"] or 0)
                chart_no_interactions.append(agg["no_interactions"] or 0)
                chart_saves.append(agg["saves"] or 0)

        return (
            chart_labels,
            chart_accepts,
            chart_declines,
            chart_shows,
            chart_no_interactions,
            chart_saves,
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        view, year, month, day = self.get_period_params()

        # Summary stats for the selected period
        stats = self.get_summary_stats(view, year, month, day)
        accepts = stats["accepts"]
        declines = stats["declines"]
        saves = stats["saves"]
        shows = stats["shows"]
        no_interactions = stats["no_interactions"]

        total = accepts + declines
        acceptance_rate = round(100 * accepts / total) if total > 0 else 0
        interactions = accepts + declines + saves
        engagement_rate = (
            min(100, round(100 * interactions / shows)) if shows > 0 else 0
        )

        prev_url, next_url, period_name = self.get_navigation_urls(
            view, year, month, day
        )

        # Chart data: adapt to view (day / month / year)
        (
            chart_labels,
            chart_accepts,
            chart_declines,
            chart_shows,
            chart_no_interactions,
            chart_saves,
        ) = self.get_chart_data(view, year, month, day)

        context.update(
            accepts=accepts,
            accepts_display=_intcomma(accepts),
            declines=declines,
            declines_display=_intcomma(declines),
            shows=shows,
            shows_display=_intcomma(shows),
            no_interactions=no_interactions,
            no_interactions_display=_intcomma(no_interactions),
            total=total,  # accepts + declines only (excludes save)
            acceptance_rate=acceptance_rate,
            engagement_rate=engagement_rate,
            period_name=period_name,
            summary_view=view,
            year=year,
            month=month,
            day=day,
            prev_url=prev_url,
            next_url=next_url,
            chart_labels=json.dumps(chart_labels),
            chart_accepts=json.dumps(chart_accepts),
            chart_declines=json.dumps(chart_declines),
            chart_shows=json.dumps(chart_shows),
            chart_no_interactions=json.dumps(chart_no_interactions),
            chart_saves=json.dumps(chart_saves),
            accepts_label=json.dumps(str(_("Accepts"))),
            declines_label=json.dumps(str(_("Declines"))),
            saves_label=json.dumps(str(_("Saves (custom)"))),
            shows_label=json.dumps(str(_("Banner displays"))),
            no_interactions_label=json.dumps(str(_("No interaction"))),
            visits_label=json.dumps(str(_("Number of visits"))),
            rate_label=json.dumps(str(_("Acceptance rate"))),
            engagement_rate_label=json.dumps(str(_("Engagement rate"))),
            no_data_msg=json.dumps(str(_("No data for this period"))),
            LANGUAGE_CODE=translation.get_language() or "en",
        )
        return context


@method_decorator(staff_member_required, name="dispatch")
class VisitorOverviewView(TemplateView):
    """
    Overview of visitor metrics (show_count) by day, month, year.
    When ENABLE_OVERVIEW is False, only Kapt users can access.
    """

    template_name = "cookie_optin/visitor_overview.html"

    def dispatch(self, request, *args, **kwargs):
        if not settings.ENABLE_OVERVIEW and not is_kapt_user(request.user):
            # Django
            from django.http import HttpResponseForbidden

            return HttpResponseForbidden()
        return super().dispatch(request, *args, **kwargs)

    def get_selected_month(self):
        now = timezone.now()
        year = int(self.request.GET.get("year", now.year))
        month = int(self.request.GET.get("month", now.month))
        if month < 1:
            month, year = 12, year - 1
        elif month > 12:
            month, year = 1, year + 1
        return year, month

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        year, month = self.get_selected_month()
        today = timezone.localdate()

        first_day = date(year, month, 1)
        last_day_num = calendar.monthrange(year, month)[1]
        last_day = date(year, month, last_day_num)

        rows = {
            r.date: r
            for r in DailyConsentStats.objects.filter(
                date__gte=first_day, date__lte=last_day
            )
        }

        chart_labels = []
        chart_visitors = []
        chart_ga_visitors = []
        month_total = 0
        ga_month_total = 0

        for day in range(1, last_day_num + 1):
            d = date(year, month, day)
            chart_labels.append(d.strftime("%d/%m"))
            r = rows.get(d)
            if r:
                chart_visitors.append(r.show_count)
                chart_ga_visitors.append(r.accept_count)
                month_total += r.show_count
                ga_month_total += r.accept_count
            else:
                chart_visitors.append(0)
                chart_ga_visitors.append(0)

        today_count = 0
        try:
            today_row = DailyConsentStats.objects.get(date=today)
            today_count = today_row.show_count
        except DailyConsentStats.DoesNotExist:
            pass

        year_start = date(year, 1, 1)
        year_end = date(year, 12, 31)
        year_total = (
            DailyConsentStats.objects.filter(
                date__gte=year_start, date__lte=year_end
            ).aggregate(total=Sum("show_count"))["total"]
            or 0
        )

        # Aggregate country_counts for the selected month (for map)
        country_counts = {}
        for r in rows.values():
            for code, count in (r.country_counts or {}).items():
                country_counts[code] = country_counts.get(code, 0) + count

        # Build translated country names (code -> localized name)
        country_names = {}
        try:
            # Third party
            from babel import Locale

            lang = (translation.get_language() or "en").split("-")[0]
            locale = Locale.parse(lang)
            for code in country_counts:
                if code == "__unknown__":
                    country_names[code] = str(_("Not geolocated"))
                else:
                    country_names[code] = locale.territories.get(code, code)
        except Exception:
            for code in country_counts:
                country_names[code] = (
                    str(_("Not geolocated")) if code == "__unknown__" else code
                )

        prev_month = month - 1 if month > 1 else 12
        prev_year = year if month > 1 else year - 1
        next_month = month + 1 if month < 12 else 1
        next_year = year if month < 12 else year + 1

        visitor_url = reverse("cookie_optin:visitor_overview")
        prev_url = f"{visitor_url}?year={prev_year}&month={prev_month}"
        next_url = f"{visitor_url}?year={next_year}&month={next_month}"

        month_name = date_format(first_day, "F Y")

        # Computed metrics for value proposition
        value_add = month_total - ga_month_total
        ga_coverage_pct = (
            round(100 * ga_month_total / month_total) if month_total else 0
        )
        invisible_pct = round(100 * value_add / month_total) if month_total else 0
        value_add_vs_ga_pct = (
            round(100 * value_add / ga_month_total) if ga_month_total else 0
        )

        context.update(
            today_count=today_count,
            today_count_display=_intcomma(today_count),
            month_total=month_total,
            month_total_display=_intcomma(month_total),
            year_total=year_total,
            year_total_display=_intcomma(year_total),
            month_name=month_name,
            year=year,
            month=month,
            prev_url=prev_url,
            next_url=next_url,
            chart_labels=json.dumps(chart_labels),
            chart_visitors=json.dumps(chart_visitors),
            chart_ga_visitors=json.dumps(chart_ga_visitors),
            visitors_label=json.dumps(str(_("Visitors (cookie optin)"))),
            ga_visitors_label=json.dumps(str(_("Accepts (≈ GA)"))),
            value_add=value_add,
            value_add_display=_intcomma(value_add),
            ga_month_total=ga_month_total,
            ga_month_total_display=_intcomma(ga_month_total),
            ga_coverage_pct=ga_coverage_pct,
            invisible_pct=invisible_pct,
            value_add_vs_ga_pct=value_add_vs_ga_pct,
            donut_ga=ga_month_total,
            donut_invisible=value_add,
            donut_ga_label=json.dumps(str(_("GA (accepted)"))),
            donut_invisible_label=json.dumps(str(_("Invisible to GA"))),
            has_ga_data=True,
            no_data_msg=json.dumps(str(_("No data for this period"))),
            country_counts=json.dumps(country_counts),
            country_names=json.dumps(country_names),
            visitors_singular=json.dumps(str(_("visitor"))),
            visitors_plural=json.dumps(str(_("visitors"))),
            hover_stats_hint=json.dumps(
                str(_("Hover over a country to see statistics"))
            ),
            theme_storage_key=json.dumps(settings.THEME_STORAGE_KEY),
            LANGUAGE_CODE=translation.get_language() or "en",
        )
        return context
