"""Feedback domain for M&EL (PRD §5.3, MFL feedback loop).

Four distinct channels surface here:

* ``ReportReview`` — stakeholder feedback on a published Report.
* ``Beneficiary`` — direct feedback from beneficiaries on outcomes.
* ``Industry``/``Alumni`` — tracer-study responses (long-term impact).
* ``REP course feedback`` — course evaluations piped in from the REP platform.

All submissions are audited via ``django-simple-history`` and can trigger
corrective actions via :func:`apps.mel.feedback.services.triage_feedback`.
"""
from __future__ import annotations

import secrets

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from simple_history.models import HistoricalRecords


# ---------------------------------------------------------------------------
# Channels
# ---------------------------------------------------------------------------


class FeedbackChannelType(models.TextChoices):
    REPORT_REVIEW = "report_review", "Report review"
    BENEFICIARY = "beneficiary", "Beneficiary"
    INDUSTRY = "industry", "Industry / employer"
    ALUMNI = "alumni", "Alumni / tracer"
    MOODLE = "moodle", "REP course feedback"
    GENERIC = "generic", "Generic"


class FeedbackChannel(models.Model):
    """A logical feedback intake. Subjects narrow scope to a Report, Indicator,
    Activity, or Project via GenericFK."""

    type = models.CharField(
        max_length=32,
        choices=FeedbackChannelType.choices,
        db_index=True,
    )
    name = models.CharField(max_length=255)
    slug = models.SlugField(max_length=120, unique=True)
    description = models.TextField(blank=True)
    # Optional subject (Report/Indicator/Activity/Project/LogFrame).
    subject_type = models.ForeignKey(
        ContentType,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_feedback_channels",
    )
    subject_id = models.PositiveIntegerField(null=True, blank=True)
    subject = GenericForeignKey("subject_type", "subject_id")

    token = models.CharField(
        max_length=48,
        unique=True,
        help_text="Opaque token used in public feedback URLs.",
    )
    is_public = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True, db_index=True)
    opens_at = models.DateTimeField(null=True, blank=True)
    closes_at = models.DateTimeField(null=True, blank=True)

    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_feedback_channels_created",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["subject_type", "subject_id"]),
            models.Index(fields=["type", "is_active"]),
        ]

    def __str__(self):
        return f"[{self.type}] {self.name}"

    @staticmethod
    def generate_token() -> str:
        return secrets.token_urlsafe(24)


# ---------------------------------------------------------------------------
# Submissions
# ---------------------------------------------------------------------------


class FeedbackSeverity(models.TextChoices):
    LOW = "low", "Low"
    MEDIUM = "medium", "Medium"
    HIGH = "high", "High"
    CRITICAL = "critical", "Critical"


class FeedbackStatus(models.TextChoices):
    NEW = "new", "New"
    TRIAGED = "triaged", "Triaged"
    ACTIONED = "actioned", "Actioned"
    CLOSED = "closed", "Closed"


class FeedbackSubmission(models.Model):
    """A single inbound feedback record."""

    channel = models.ForeignKey(
        FeedbackChannel,
        on_delete=models.CASCADE,
        related_name="submissions",
    )
    # Submitter identity is optional to support anonymous/public intake.
    submitter_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_feedback_submitted",
    )
    submitter_email = models.EmailField(blank=True)
    submitter_name = models.CharField(max_length=255, blank=True)

    rating = models.SmallIntegerField(
        null=True,
        blank=True,
        help_text="1–5 Likert value (optional).",
    )
    narrative = models.TextField()
    severity = models.CharField(
        max_length=16,
        choices=FeedbackSeverity.choices,
        default=FeedbackSeverity.LOW,
        db_index=True,
    )
    status = models.CharField(
        max_length=16,
        choices=FeedbackStatus.choices,
        default=FeedbackStatus.NEW,
        db_index=True,
    )

    # Idempotency key: callers can pass a stable token (form HMAC, webhook
    # event id) so replays collapse into a single submission.
    submission_key = models.CharField(max_length=128, blank=True, db_index=True)

    corrective_action = models.ForeignKey(
        "mel_tracking.CorrectiveAction",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="triggered_by_feedback",
    )

    created_at = models.DateTimeField(auto_now_add=True)
    triaged_at = models.DateTimeField(null=True, blank=True)
    triaged_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_feedback_triaged",
    )

    history = HistoricalRecords()

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["channel", "status"]),
            models.Index(fields=["submission_key"]),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=["channel", "submission_key"],
                condition=~models.Q(submission_key=""),
                name="uniq_feedback_submission_key",
            ),
        ]

    def __str__(self):
        return f"[{self.status}] {self.channel.name}"


# ---------------------------------------------------------------------------
# Tracer studies (alumni + industry)
# ---------------------------------------------------------------------------


class TracerStudy(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(max_length=120, unique=True)
    description = models.TextField(blank=True)
    target_population = models.TextField(
        blank=True,
        help_text="E.g. 'Graduates of PhD cohort 2023'.",
    )
    opens_at = models.DateField(null=True, blank=True)
    closes_at = models.DateField(null=True, blank=True)
    is_active = models.BooleanField(default=True)
    # G6 — post-project tracer dispatch.
    dispatch_after_logframe_close = models.ForeignKey(
        "mel_indicators.LogFrame",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="post_project_tracers",
        help_text=(
            "G6 — when set, this tracer auto-dispatches once the linked log "
            "frame has been closed for at least its post_project_window_months."
        ),
    )
    dispatched_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="Stamped by the G6 post-project beat to enforce one-shot dispatch.",
    )
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name


class TracerResponse(models.Model):
    study = models.ForeignKey(
        TracerStudy,
        on_delete=models.CASCADE,
        related_name="responses",
    )
    respondent_email = models.EmailField(blank=True)
    respondent_name = models.CharField(max_length=255, blank=True)
    employment_status = models.CharField(max_length=120, blank=True)
    employer = models.CharField(max_length=255, blank=True)
    role = models.CharField(max_length=255, blank=True)
    salary_band = models.CharField(max_length=60, blank=True)
    payload = models.JSONField(default=dict, blank=True)
    submitted_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-submitted_at"]
        indexes = [
            models.Index(fields=["study", "submitted_at"]),
        ]

    def __str__(self):
        return f"{self.study.slug} response"


# ---------------------------------------------------------------------------
# Course evaluations (mirror of Moodle Feedback activity output)
# ---------------------------------------------------------------------------


class CourseEvaluation(models.Model):
    source_system = models.CharField(
        max_length=32,
        default="moodle",
        help_text="E.g. 'moodle', 'rep'.",
    )
    course_slug = models.CharField(max_length=255)
    course_name = models.CharField(max_length=255, blank=True)
    period_label = models.CharField(max_length=40, blank=True)
    aggregate_score = models.DecimalField(
        max_digits=6, decimal_places=3, null=True, blank=True,
    )
    response_count = models.PositiveIntegerField(default=0)
    payload = models.JSONField(default=dict, blank=True)
    ingested_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-ingested_at"]
        indexes = [
            models.Index(fields=["source_system", "course_slug", "period_label"]),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=["source_system", "course_slug", "period_label"],
                name="uniq_course_evaluation_period",
            ),
        ]

    def __str__(self):
        return f"{self.source_system}:{self.course_slug} {self.period_label}".strip()


# ---------------------------------------------------------------------------
# Phase 4.3 — Survey builder (FRSME-MEI017–019, FRMFL028–030)
# ---------------------------------------------------------------------------
#
# The Survey layer wraps the existing FeedbackChannel/FeedbackSubmission
# pipeline. Activating a Survey provisions the backing FeedbackChannel and
# token-URL machinery; responses are stored as SurveyResponse rows that are
# 1-to-1 with FeedbackSubmission so the triage/corrective-action paths keep
# working unchanged.


class SurveyStatus(models.TextChoices):
    DRAFT = "draft", "Draft"
    ACTIVE = "active", "Active"
    CLOSED = "closed", "Closed"


class SurveyTrigger(models.TextChoices):
    MANUAL = "manual", "Manual dispatch"
    POST_INCUBATION = "post_incubation", "Post-incubation"
    POST_CONNECTION = "post_connection", "Post-connection"
    POST_DEAL = "post_deal", "Post-deal"
    POST_COURSE = "post_course", "Post-course completion"
    POST_GRADUATION = "post_graduation", "Post-graduation"


class Survey(models.Model):
    """Configurable survey instrument — questions, audience, dispatch trigger."""

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=140, unique=True)
    description = models.TextField(blank=True)
    status = models.CharField(
        max_length=16,
        choices=SurveyStatus.choices,
        default=SurveyStatus.DRAFT,
        db_index=True,
    )
    trigger = models.CharField(
        max_length=24,
        choices=SurveyTrigger.choices,
        default=SurveyTrigger.MANUAL,
    )
    audience = models.JSONField(
        default=dict,
        blank=True,
        help_text="Filter spec: {'programmes': [...], 'cohorts': [...], 'roles': [...]}.",
    )
    response_window_days = models.PositiveIntegerField(
        default=14,
        help_text="Reminder fires when a dispatched survey has no response after this window.",
    )
    is_anonymous = models.BooleanField(default=False)
    version = models.PositiveIntegerField(default=1)
    channel = models.OneToOneField(
        FeedbackChannel,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="survey",
        help_text="FeedbackChannel created on activation.",
    )

    activated_at = models.DateTimeField(null=True, blank=True)
    closed_at = models.DateTimeField(null=True, blank=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_surveys_created",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["status", "trigger"]),
        ]

    def __str__(self):
        return self.title

    @property
    def is_active(self) -> bool:
        return self.status == SurveyStatus.ACTIVE


class SurveyQuestionType(models.TextChoices):
    SHORT_ANSWER = "short_answer", "Short answer"
    ESSAY = "essay", "Essay"
    MULTIPLE_CHOICE = "multiple_choice", "Multiple choice"
    RATING = "rating", "Rating scale"
    NPS = "nps", "Net Promoter Score (0–10)"
    BOOLEAN = "boolean", "Yes / No"


class SurveyQuestion(models.Model):
    survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name="questions")
    ordinal = models.PositiveIntegerField(default=0)
    page = models.PositiveIntegerField(
        default=1,
        help_text="Survey-take splits questions across pages by this number.",
    )
    type = models.CharField(max_length=24, choices=SurveyQuestionType.choices)
    text = models.TextField()
    help_text = models.CharField(max_length=255, blank=True)
    required = models.BooleanField(default=False)
    options = models.JSONField(
        default=list,
        blank=True,
        help_text="List of {value,label} for choice/rating; rating uses {min,max,labels}.",
    )

    class Meta:
        ordering = ["survey_id", "ordinal", "pk"]
        indexes = [
            models.Index(fields=["survey", "page", "ordinal"]),
        ]

    def __str__(self):
        return f"Q{self.ordinal}: {self.text[:80]}"

    def rating_points(self) -> list[int]:
        """Ordered list of selectable values for a RATING question.

        Rating bounds are stored on ``options`` as ``{"min": lo, "max": hi}``
        (see the survey builder). Falls back to a 1–5 scale when unset or
        malformed, and guards against pathological ranges.
        """
        opts = self.options if isinstance(self.options, dict) else {}
        try:
            lo = int(opts.get("min", 1))
        except (TypeError, ValueError):
            lo = 1
        try:
            hi = int(opts.get("max", 5))
        except (TypeError, ValueError):
            hi = 5
        if hi < lo:
            lo, hi = hi, lo
        if hi - lo > 100:
            hi = lo + 100
        return list(range(lo, hi + 1))


class SurveyDispatch(models.Model):
    """One row per (survey, recipient) intent. Tracks reminder + non-response."""

    class Status(models.TextChoices):
        SENT = "sent", "Sent — awaiting response"
        REMINDED = "reminded", "Reminder sent"
        COMPLETED = "completed", "Completed"
        NON_RESPONDED = "non_responded", "Non-responded"
        CANCELLED = "cancelled", "Cancelled"

    survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name="dispatches")
    recipient = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_survey_dispatches",
    )
    recipient_email = models.EmailField(blank=True)
    token = models.CharField(max_length=48, unique=True)
    trigger_event = models.CharField(max_length=128, blank=True)
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.SENT,
        db_index=True,
    )
    dispatched_at = models.DateTimeField(auto_now_add=True)
    due_at = models.DateTimeField(null=True, blank=True)
    reminded_at = models.DateTimeField(null=True, blank=True)
    completed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-dispatched_at"]
        indexes = [
            models.Index(fields=["survey", "status"]),
            models.Index(fields=["status", "due_at"]),
        ]

    def __str__(self):
        return f"Dispatch {self.survey_id}→{self.recipient_id or self.recipient_email}"

    @staticmethod
    def generate_token() -> str:
        return secrets.token_urlsafe(24)


class SurveyResponse(models.Model):
    class Status(models.TextChoices):
        IN_PROGRESS = "in_progress", "In progress"
        COMPLETED = "completed", "Completed"

    survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name="responses")
    dispatch = models.ForeignKey(
        SurveyDispatch,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="responses",
    )
    submission = models.OneToOneField(
        FeedbackSubmission,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="survey_response",
        help_text="Created on completion so triage/corrective-action receivers fire.",
    )
    respondent = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="mel_survey_responses",
    )
    respondent_email = models.EmailField(blank=True)
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.IN_PROGRESS,
        db_index=True,
    )
    started_at = models.DateTimeField(auto_now_add=True)
    completed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-started_at"]
        indexes = [
            models.Index(fields=["survey", "status"]),
        ]

    def __str__(self):
        return f"Response {self.pk} ({self.survey_id})"


class SurveyAnswer(models.Model):
    response = models.ForeignKey(
        SurveyResponse,
        on_delete=models.CASCADE,
        related_name="answers",
    )
    question = models.ForeignKey(
        SurveyQuestion,
        on_delete=models.CASCADE,
        related_name="answers",
    )
    value = models.JSONField(default=dict, blank=True)
    free_text = models.TextField(blank=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["response", "question"],
                name="uniq_survey_answer_per_question",
            ),
        ]

    def __str__(self):
        return f"Ans Q{self.question_id} R{self.response_id}"


class SatisfactionScore(models.Model):
    """Denormalised aggregate rebuilt by ``analyse_responses(survey)``."""

    class Dimension(models.TextChoices):
        NPS = "nps", "Net Promoter Score"
        MEAN_RATING = "mean_rating", "Mean rating"
        COMPLETION_RATE = "completion_rate", "Completion rate"
        NON_RESPONSE_RATE = "non_response_rate", "Non-response rate"
        COUNT = "count", "Count"

    survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name="scores")
    question = models.ForeignKey(
        SurveyQuestion,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="scores",
    )
    dimension = models.CharField(max_length=24, choices=Dimension.choices)
    value = models.DecimalField(max_digits=10, decimal_places=4)
    sample_size = models.PositiveIntegerField(default=0)
    payload = models.JSONField(default=dict, blank=True)
    computed_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-computed_at"]
        indexes = [
            models.Index(fields=["survey", "dimension"]),
        ]

    def __str__(self):
        return f"{self.survey_id}/{self.dimension}={self.value}"
