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 django.urls import reverse


class Notification(models.Model):
    class Verb(models.TextChoices):
        GRANT_AWARDED = "grant_awarded", "Grant awarded"
        # PRD §5.1 FRFA-AA016/AA017 — cancellation workflow notifications.
        AWARD_CANCELLATION_REQUESTED = "award_cancellation_requested", "Award cancellation — awaiting review"
        AWARD_CANCELLATION_APPROVED = "award_cancellation_approved", "Award cancellation — approved"
        AWARD_CANCELLATION_REJECTED = "award_cancellation_rejected", "Award cancellation — rejected"
        # PRD §5.1 FRFA-AA023-030 — cohort addendum workflow notifications.
        COHORT_ADDENDUM_REQUESTED = "cohort_addendum_requested", "Cohort addendum — awaiting review"
        COHORT_ADDENDUM_APPROVED = "cohort_addendum_approved", "Cohort addendum — approved"
        COHORT_ADDENDUM_REJECTED = "cohort_addendum_rejected", "Cohort addendum — rejected"
        DEADLINE_REMINDER = "deadline_reminder", "Deadline reminder"
        APPLICATION_STATUS = "application_status", "Application status"
        # PRD §5.1 FRFA-CS023/CS025 — submission acknowledgement on successful applicant submit.
        APPLICATION_RECEIVED_ACK = "application_received_ack", "Application received — acknowledgement"
        CALL_PUBLISHED = "call_published", "Call published"
        REVIEWER_ASSIGNED = "reviewer_assigned", "Reviewer assigned"
        REVIEW_INTAKE_MANAGER = "review_intake_manager", "Grants — assign reviewers"
        INTERVIEW_COMPLETED = "interview_completed", "Interview completed"
        MILESTONE_OVERDUE = "milestone_overdue", "Milestone overdue"
        FINANCE_DISBURSEMENT = "finance_disbursement", "Finance disbursement"
        REP_CERTIFICATE_ISSUED = "rep_certificate_issued", "Course certificate issued"
        REP_COURSE_PUBLISHED = "rep_course_published", "REP course published"
        REP_COURSE_UNPUBLISHED = "rep_course_unpublished", "REP course returned to draft"
        REP_COURSE_REVIEW_SUBMITTED = "rep_course_review_submitted", "REP course submitted for publish review"
        REP_COURSE_REVIEW_RETURNED = "rep_course_review_returned", "REP publish review returned"
        REP_ASSIGNMENT_DEADLINE_EXTENDED = "rep_assignment_deadline_extended", "REP assignment deadline extended"
        # REP — SP1 (UR) profile / role / bulk
        REP_PROFILE_UPDATED = "rep_profile_updated", "REP — profile updated"
        REP_ROLE_CHANGED = "rep_role_changed", "REP — role changed"
        REP_BULK_IMPORT_COMPLETE = "rep_bulk_import_complete", "REP — bulk user import complete"
        # REP — SP2 (CM) course lifecycle
        REP_COURSE_ARCHIVED = "rep_course_archived", "REP — course archived"
        REP_COURSE_SIGNIFICANT_UPDATE = "rep_course_significant_update", "REP — significant course update"
        REP_COURSE_REVISION_PUBLISHED = "rep_course_revision_published", "REP — course revision published"
        # REP — SP3 (EN) enrolment / waitlist
        REP_ENROLMENT_CONFIRMED = "rep_enrolment_confirmed", "REP — enrolment confirmed"
        REP_ENROLMENT_APPROVED = "rep_enrolment_approved", "REP — enrolment approved"
        REP_ENROLMENT_REJECTED = "rep_enrolment_rejected", "REP — enrolment rejected"
        REP_RIMS_AUTO_ENROLLED = "rep_rims_auto_enrolled", "REP — auto-enrolled via scholarship"
        REP_WAITLIST_PROMOTED = "rep_waitlist_promoted", "REP — promoted from waitlist"
        # REP — SP4 (LD) live session / forum
        REP_LIVE_SESSION_SCHEDULED = "rep_live_session_scheduled", "REP — live session scheduled"
        REP_LIVE_SESSION_STARTING = "rep_live_session_starting", "REP — live session starting soon"
        REP_LIVE_SESSION_CANCELLED = "rep_live_session_cancelled", "REP — live session cancelled"
        REP_LIVE_RECORDING_READY = "rep_live_recording_ready", "REP — session recording available"
        REP_WIKI_PAGE_UPDATED = "rep_wiki_page_updated", "REP — wiki page updated"
        REP_GLOSSARY_ENTRY_PENDING = "rep_glossary_entry_pending", "REP — glossary term awaiting review"
        REP_GLOSSARY_ENTRY_APPROVED = "rep_glossary_entry_approved", "REP — glossary term approved"
        REP_GLOSSARY_ENTRY_REJECTED = "rep_glossary_entry_rejected", "REP — glossary term rejected"
        REP_FORUM_REPLY = "rep_forum_reply", "REP — forum reply"
        REP_FORUM_POST_FLAGGED = "rep_forum_post_flagged", "REP — forum post flagged for review"
        REP_FORUM_MODERATION_OUTCOME = "rep_forum_moderation_outcome", "REP — forum moderation outcome"
        # REP — SP5 (AG) assessment / grading
        REP_ASSIGNMENT_SUBMITTED = "rep_assignment_submitted", "REP — assignment submitted"
        REP_ASSIGNMENT_GRADED = "rep_assignment_graded", "REP — assignment graded"
        REP_QUIZ_AUTO_GRADED = "rep_quiz_auto_graded", "REP — quiz auto-graded"
        REP_LATE_SUBMISSION = "rep_late_submission", "REP — late submission recorded"
        REP_GRADE_OVERRIDE_APPLIED = "rep_grade_override_applied", "REP — grade override applied"
        REP_GRADE_APPEAL_SUBMITTED = "rep_grade_appeal_submitted", "REP — grade appeal submitted"
        REP_WORKSHOP_PEER_REVIEW_ASSIGNED = "rep_workshop_peer_review_assigned", "REP — peer review assigned"
        # REP — content delivery (FRREP-LD009 alt-flow A3)
        REP_SCORM_ERROR = "rep_scorm_error", "REP — SCORM package error"
        # REP — SP6 (CC) certification
        REP_CERTIFICATE_REVOKED = "rep_certificate_revoked", "REP — certificate revoked"
        # REP — SP7 (NW) networking
        REP_CONNECTION_REQUEST = "rep_connection_request", "REP — connection request received"
        REP_CONNECTION_ACCEPTED = "rep_connection_accepted", "REP — connection accepted"
        REP_DM_RECEIVED = "rep_dm_received", "REP — direct message received"
        REP_MENTOR_MATCHED = "rep_mentor_matched", "REP — matched with mentor"
        # REP — SP8 (TS) career
        REP_JOB_LISTING_MATCH = "rep_job_listing_match", "REP — job listing match"
        REP_CV_EXPORT_READY = "rep_cv_export_ready", "REP — CV export ready"
        REP_JOB_APPLICATION_DECIDED = "rep_job_application_decided", "REP — job application decision"
        # REP — SP9 (PL) personalisation
        REP_RECOMMENDATION_SURFACED = "rep_recommendation_surfaced", "REP — recommendation surfaced"
        # REP — SP10 (AR) reporting / M&EL
        REP_REPORT_READY = "rep_report_ready", "REP — report ready for download"
        REP_MEL_PUSH_FAILED = "rep_mel_push_failed", "REP — M&EL push failure alert"
        REPO_DOC_SUBMITTED = "repo_doc_submitted", "Repository document submitted"
        REPO_QA_ASSIGNED = "repo_qa_assigned", "Repository QA review assigned"
        REPO_DOC_APPROVED = "repo_doc_approved", "Repository document approved"
        REPO_DOC_REJECTED = "repo_doc_rejected", "Repository document rejected"
        REPO_DOC_REVISION_REQUIRED = "repo_doc_revision_required", "Repository document revision required"
        REPO_DOC_PUBLISHED = "repo_doc_published", "Repository document published"
        REPO_DOC_WITHDRAWN = "repo_doc_withdrawn", "Repository document withdrawn"
        REPO_ACCESS_REQUESTED = "repo_access_requested", "Repository access requested"
        REPO_ACCESS_GRANTED = "repo_access_granted", "Repository access granted"
        REPO_ACCESS_DENIED = "repo_access_denied", "Repository access denied"
        REPO_RETENTION_DUE = "repo_retention_due", "Repository retention due"
        REPO_RETENTION_NOTICE = "repo_retention_notice", "Repository retention — advance notice"
        REPO_REP_LINK_BROKEN = "repo_rep_link_broken", "REP link to withdrawn document"
        REPO_REVIEW_TASK_ASSIGNED = "repo_review_task_assigned", "Repository review task assigned"
        REPO_REVIEW_TASK_DUE = "repo_review_task_due", "Repository review task due soon"
        # FRREP-COL005 — the assignee/owner-facing loop when a review task's
        # status changes (in particular, when it's marked complete).
        REPO_REVIEW_TASK_COMPLETED = "repo_review_task_completed", "Repository review task completed"
        # FRREP-COL008 — both editors are told a concurrent edit was rejected.
        REPO_EDIT_CONFLICT = "repo_edit_conflict", "Repository — concurrent edit conflict"
        REPO_DOC_SHARED = "repo_doc_shared", "Repository document shared with you"
        REPO_QA_OVERDUE = "repo_qa_overdue", "Repository QA review overdue"
        REPO_SAVED_SEARCH_ALERT = "repo_saved_search_alert", "Repository saved-search alert"
        REPO_DOC_INGESTED = "repo_doc_ingested", "Repository document ingested"
        REPO_TAG_SUGGESTION_PENDING = "repo_tag_suggestion_pending", "Repository tag awaiting approval"
        REPO_COMMENT_RESOLVED = "repo_comment_resolved", "Repository comment resolved"
        REPO_AI_INSIGHT_ACCEPTED = "repo_ai_insight_accepted", "Repository AI insight accepted"
        # FRREP-VL007 — notify bookmarkers (not just the submitter) on withdrawal.
        REPO_SAVED_DOC_WITHDRAWN = "repo_saved_doc_withdrawn", "Repository — saved document withdrawn"
        # FRREP-DCI007 — a ClamAV positive must reach Repository administrators,
        # not just a python logger.
        REPO_UPLOAD_MALWARE_DETECTED = "repo_upload_malware_detected", "Repository — malware detected on upload"
        # SP6 two-tier review (2026-07 interviews) — reviewer recommendation
        # awaiting an Editor-in-Chief final decision / compliance flag raised.
        REPO_QA_RECOMMENDED = "repo_qa_recommended", "Repository — QA recommendation recorded"
        REPO_QA_COMPLIANCE_FLAGGED = "repo_qa_compliance_flagged", "Repository — compliance check flagged"
        MEL_INDICATOR_OFF_TRACK = "mel_indicator_off_track", "M&EL — indicator off track"
        MEL_ACTIVITY_DELAYED = "mel_activity_delayed", "M&EL — activity delayed"
        MEL_DATA_POINT_REJECTED = "mel_data_point_rejected", "M&EL — data point rejected"
        MEL_DATA_POINT_CLARIFICATION = "mel_data_point_clarification", "M&EL — data point clarification requested"
        MEL_EVIDENCE_REQUIRED = "mel_evidence_required", "M&EL — supporting evidence required"
        MEL_COMPLIANCE_ALERT = "mel_compliance_alert", "M&EL — compliance issue raised"
        MEL_COMPLIANCE_ESCALATION = "mel_compliance_escalation", "M&EL — critical compliance breach escalated"
        MEL_FEEDBACK_REMINDER = "mel_feedback_reminder", "M&EL — survey reminder"
        MEL_FEEDBACK_RESOLVED = "mel_feedback_resolved", "M&EL — feedback resolved"
        MEL_REPORT_PUBLISHED = "mel_report_published", "M&EL — report published"
        MEL_SURVEY_INVITE = "mel_survey_invite", "M&EL — survey invitation"
        MEL_OUTPUT_READY_TO_CLOSE = "mel_output_ready_to_close", "M&EL — output ready to close"
        MEL_INDICATOR_SILENT = "mel_indicator_silent", "M&EL — indicator overdue (no data)"
        MEL_INDICATOR_ESCALATION_TIER2 = "mel_indicator_escalation_tier2", "M&EL — alert escalated to tier 2"
        MEL_INDICATOR_ESCALATION_TIER3 = "mel_indicator_escalation_tier3", "M&EL — alert escalated to tier 3"
        ALUMNI_PROFILE_CREATED = "alumni_profile_created", "Alumni — profile created"
        ALUMNI_MILESTONE_ADDED = "alumni_milestone_added", "Alumni — milestone added"
        ALUMNI_ORCID_SYNC_FAILED = "alumni_orcid_sync_failed", "Alumni — ORCID sync failed"
        ALUMNI_MENTORSHIP_REQUESTED = "alumni_mentorship_requested", "Alumni — mentorship requested"
        ALUMNI_MENTORSHIP_ACCEPTED = "alumni_mentorship_accepted", "Alumni — mentorship accepted"
        ALUMNI_MENTORSHIP_DECLINED = "alumni_mentorship_declined", "Alumni — mentorship declined"
        ALUMNI_EVENT_REGISTERED = "alumni_event_registered", "Alumni — event registered"
        ALUMNI_EVENT_REMINDER = "alumni_event_reminder", "Alumni — event reminder"
        ALUMNI_EVENT_SUBMITTED = "alumni_event_submitted", "Alumni — event submitted for approval"
        ALUMNI_EVENT_APPROVED = "alumni_event_approved", "Alumni — event approved"
        ALUMNI_EVENT_REJECTED = "alumni_event_rejected", "Alumni — event rejected"
        ALUMNI_TRACER_SURVEY_INVITED = "alumni_tracer_survey_invited", "Alumni — tracer survey invitation"
        ALUMNI_GROUP_NEW_EVENT = "alumni_group_new_event", "Alumni — new event in your group"
        ALUMNI_GROUP_EVENT_NUDGE = "alumni_group_event_nudge", "Alumni — upcoming group event reminder"
        ALUMNI_GROUP_ANNOUNCEMENT = "alumni_group_announcement", "Alumni — group announcement"
        ALUMNI_GROUP_MEMBER_PROMOTED = "alumni_group_member_promoted", "Alumni — promoted to group moderator"
        ALUMNI_CHAPTER_LEAD_REQUESTED = "alumni_chapter_lead_requested", "Alumni — chapter leadership requested"
        ALUMNI_CHAPTER_LEAD_APPROVED = "alumni_chapter_lead_approved", "Alumni — chapter leadership approved"
        ALUMNI_CHAPTER_LEAD_DECLINED = "alumni_chapter_lead_declined", "Alumni — chapter leadership declined"
        ALUMNI_MENTORSHIP_SESSION_LOGGED = "alumni_mentorship_session_logged", "Alumni — mentorship session logged"
        ALUMNI_MENTORSHIP_COMPLETED = "alumni_mentorship_completed", "Alumni — mentorship completed"
        ALUMNI_MENTORSHIP_WITHDRAWN = "alumni_mentorship_withdrawn", "Alumni — mentorship request withdrawn"
        ALUMNI_SPOTLIGHT_ENDORSED = "alumni_spotlight_endorsed", "Alumni — spotlight endorsement received"
        ALUMNI_SPOTLIGHT_FEATURED = "alumni_spotlight_featured", "Alumni — selected for a spotlight"
        ALUMNI_SPOTLIGHT_DIGEST = "alumni_spotlight_digest", "Alumni — spotlight digest"
        ALUMNI_BROADCAST = "alumni_broadcast", "Alumni — officer broadcast"
        ALUMNI_JOB_SUBMITTED = "alumni_job_submitted", "Alumni — partner job submitted"
        ALUMNI_JOB_APPROVED = "alumni_job_approved", "Alumni — partner job approved"
        ALUMNI_EMPLOYER_FEEDBACK_INVITED = "alumni_employer_feedback_invited", "Alumni — employer feedback invited"
        # SME-Hub — onboarding (SP1)
        SMEHUB_REGISTRATION_VERIFY_EMAIL = "smehub_registration_verify_email", "SME-Hub — verify your email"
        SMEHUB_ENTREPRENEUR_PENDING_REVIEW = "smehub_entrepreneur_pending_review", "SME-Hub — entrepreneur awaiting review"
        SMEHUB_ENTREPRENEUR_VERIFIED = "smehub_entrepreneur_verified", "SME-Hub — entrepreneur verified"
        SMEHUB_ENTREPRENEUR_REJECTED = "smehub_entrepreneur_rejected", "SME-Hub — entrepreneur rejected"
        SMEHUB_ENTREPRENEUR_MORE_INFO = "smehub_entrepreneur_more_info", "SME-Hub — more information requested"
        SMEHUB_BUSINESS_PENDING_REVIEW = "smehub_business_pending_review", "SME-Hub — business awaiting review"
        SMEHUB_NEW_TRACKING_RECORD = "smehub_new_tracking_record", "SME-Hub — new M&E tracking record"
        SMEHUB_ROLE_PROFILE_VERIFIED = "smehub_role_profile_verified", "SME-Hub — role profile verified"
        SMEHUB_BUSINESS_VERIFIED = "smehub_business_verified", "SME-Hub — business verified"
        SMEHUB_BUSINESS_REJECTED = "smehub_business_rejected", "SME-Hub — business rejected"
        SMEHUB_BUSINESS_REVOKED = "smehub_business_revoked", "SME-Hub — business revoked"
        # SME-Hub — incubation (SP2)
        SMEHUB_PROGRAMME_PUBLISHED = "smehub_programme_published", "SME-Hub — incubation programme published"
        SMEHUB_PROGRAMME_ELIGIBLE = "smehub_programme_eligible", "SME-Hub — you are eligible for a programme"
        SMEHUB_APPLICATION_RECEIVED = "smehub_application_received", "SME-Hub — application received"
        SMEHUB_APPLICATION_RETURNED = "smehub_application_returned", "SME-Hub — application returned for info"
        SMEHUB_JUDGE_ASSIGNED = "smehub_judge_assigned", "SME-Hub — judge assignment"
        SMEHUB_JUDGE_REMINDER = "smehub_judge_reminder", "SME-Hub — judge scoring reminder"
        SMEHUB_COHORT_ACCEPTED = "smehub_cohort_accepted", "SME-Hub — accepted into cohort"
        SMEHUB_COHORT_REJECTED = "smehub_cohort_rejected", "SME-Hub — application not selected"
        SMEHUB_MENTOR_ASSIGNED = "smehub_mentor_assigned", "SME-Hub — mentor assigned"
        SMEHUB_SESSION_SCHEDULED = "smehub_session_scheduled", "SME-Hub — mentorship session scheduled"
        SMEHUB_SESSION_CANCELLED = "smehub_session_cancelled", "SME-Hub — mentorship session cancelled"
        SMEHUB_MEETING_REQUESTED = "smehub_meeting_requested", "SME-Hub — meeting request received"
        SMEHUB_MEETING_ACCEPTED = "smehub_meeting_accepted", "SME-Hub — meeting request accepted"
        SMEHUB_MEETING_DECLINED = "smehub_meeting_declined", "SME-Hub — meeting request declined"
        SMEHUB_PROGRAMME_COMPLETE = "smehub_programme_complete", "SME-Hub — programme completed"
        SMEHUB_CERTIFICATE_ISSUED = "smehub_certificate_issued", "SME-Hub — certificate issued"
        SMEHUB_READINESS_RECORDED = "smehub_readiness_recorded", "SME-Hub — readiness assessment recorded"
        # SME-Hub — linkage / marketplace (SP3)
        SMEHUB_CONNECTION_REQUESTED = "smehub_connection_requested", "SME-Hub — connection request received"
        SMEHUB_CONNECTION_ACCEPTED = "smehub_connection_accepted", "SME-Hub — connection accepted"
        SMEHUB_CONNECTION_DECLINED = "smehub_connection_declined", "SME-Hub — connection declined"
        SMEHUB_MOU_DRAFTED = "smehub_mou_drafted", "SME-Hub — MoU draft ready for review"
        SMEHUB_MOU_FORMALISED = "smehub_mou_formalised", "SME-Hub — MoU formalised"
        SMEHUB_MARKET_DEMAND_MATCH = "smehub_market_demand_match", "SME-Hub — new market demand for you"
        SMEHUB_DEMAND_RESPONSE = "smehub_demand_response", "SME-Hub — demand response received"
        SMEHUB_ADVISORY_BOOKED = "smehub_advisory_booked", "SME-Hub — advisory session booked"
        SMEHUB_ADVISORY_CANCELLED = "smehub_advisory_cancelled", "SME-Hub — advisory session cancelled"
        # SME-Hub — showcasing & deal management (SP4)
        SMEHUB_SHOWCASE_PUBLISHED = "smehub_showcase_published", "SME-Hub — showcase event published"
        SMEHUB_CHALLENGE_PUBLISHED = "smehub_challenge_published", "SME-Hub — innovation challenge open"
        SMEHUB_SHOWCASE_PARTICIPANT_CONFIRMED = "smehub_showcase_participant_confirmed", "SME-Hub — confirmed for showcase"
        SMEHUB_DEAL_ROOM_OPENED = "smehub_deal_room_opened", "SME-Hub — deal room opened"
        SMEHUB_DEAL_ROOM_MESSAGE = "smehub_deal_room_message", "SME-Hub — new message in deal room"
        SMEHUB_DEAL_ROOM_DOCUMENT = "smehub_deal_room_document", "SME-Hub — new document in deal room"
        SMEHUB_DEAL_ROOM_EXPIRED = "smehub_deal_room_expired", "SME-Hub — deal room expired"
        SMEHUB_AGREEMENT_FORMALISED = "smehub_agreement_formalised", "SME-Hub — agreement formalised"
        # SME-Hub — investment (SP5)
        SMEHUB_FUNDING_CALL_PUBLISHED = "smehub_funding_call_published", "SME-Hub — funding call published"
        SMEHUB_FUNDING_CALL_MATCH = "smehub_funding_call_match", "SME-Hub — funding match for your business"
        SMEHUB_FUNDING_OUTCOME = "smehub_funding_outcome", "SME-Hub — funding application outcome"
        SMEHUB_INVESTOR_REQUEST = "smehub_investor_request", "SME-Hub — investor pitch request"
        SMEHUB_INVESTOR_ACCEPTED = "smehub_investor_accepted", "SME-Hub — investor accepted your request"
        SMEHUB_INVESTOR_DECLINED = "smehub_investor_declined", "SME-Hub — investor declined your request"
        SMEHUB_FUNDING_SECURED = "smehub_funding_secured", "SME-Hub — funding secured"
        SMEHUB_FEEDBACK_SURVEY = "smehub_feedback_survey", "SME-Hub — share your feedback"
        # SME-Hub — innovation onboarding & vetting (Phase 6.1)
        SMEHUB_INNOVATION_SUBMITTED = "smehub_innovation_submitted", "SME-Hub — innovation submitted for vetting"
        SMEHUB_INNOVATION_EVALUATOR_ASSIGNED = "smehub_innovation_evaluator_assigned", "SME-Hub — assigned to vet an innovation"
        SMEHUB_INNOVATION_DECISION = "smehub_innovation_decision", "SME-Hub — innovation vetting decision"
        SMEHUB_INNOVATION_REVISIONS = "smehub_innovation_revisions", "SME-Hub — innovation revisions requested"
        SMEHUB_INNOVATION_PUBLISHED = "smehub_innovation_published", "SME-Hub — innovation published to catalogue"
        SMEHUB_PRODUCT_INTEREST = "smehub_product_interest", "SME-Hub — interest expressed in your product"
        # SME-Hub — marketplace validation & sales (Phase 6.3)
        SMEHUB_MARKET_VALIDATION_RECORDED = "smehub_market_validation_recorded", "SME-Hub — market validation recorded"
        SMEHUB_SALE_STUB_CREATED = "smehub_sale_stub_created", "SME-Hub — complete the awarded sale"
        SMEHUB_SALE_VERIFIED = "smehub_sale_verified", "SME-Hub — sale verified"
        SMEHUB_SALE_DISPUTED = "smehub_sale_disputed", "SME-Hub — sale disputed"
        # SME-Hub — MEL admin alerts
        SMEHUB_RECORDS_STALLED = "smehub_records_stalled", "SME-Hub — entrepreneurs flagged stalled"
        SMEHUB_FEED_HANDLER_FAILED = "smehub_feed_handler_failed", "SME-Hub — M&E feed handler failure"
        SMEHUB_ENROLMENT_FAILED = "smehub_enrolment_failed", "SME-Hub — REP enrolment failed for cohort member"
        # RIMS P1 — flow & notification completeness
        RIMS_SCHOLAR_STATUS_CHANGED = "rims_scholar_status_changed", "RIMS — scholar status changed"
        RIMS_MOU_EXPIRY_WARNING = "rims_mou_expiry_warning", "RIMS — MOU expiring soon"
        RIMS_MOU_EXPIRED = "rims_mou_expired", "RIMS — MOU expired"
        RIMS_FUNDING_RECORD_CLOSED = "rims_funding_record_closed", "RIMS — funding record closed"
        RIMS_TRANCHE_APPROVED = "rims_tranche_approved", "RIMS — tranche approved"
        # RIMS Phase 3 — Application + reviewer integrity (FRFA-AM017, FRFA-AM029)
        APPLICATION_READY_FOR_REVIEW = (
            "application_ready_for_review",
            "Application ready for review",
        )
        REVIEWER_COI_DECLARED = (
            "reviewer_coi_declared",
            "Reviewer declared a conflict of interest",
        )
        GENERIC = "generic", "Generic"

    recipient = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="notifications",
    )
    verb = models.CharField(max_length=64, choices=Verb.choices, default=Verb.GENERIC)
    message = models.TextField()
    is_read = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE)
    object_id = models.CharField(max_length=64, null=True, blank=True)
    target = GenericForeignKey("content_type", "object_id")
    action_url = models.CharField(max_length=500, blank=True, help_text="Relative URL to navigate to when notification is clicked.")
    data = models.JSONField(
        default=dict,
        blank=True,
        help_text=(
            "Verb-specific render context for in-app surfaces (inbox / dropdown). "
            "Distinct from email_context — email_context is render-time and not persisted. "
            "Keys are convention-driven per verb (see _verb_*.html partials)."
        ),
    )
    email_sent = models.BooleanField(default=False)
    email_error = models.TextField(blank=True)

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

    @property
    def in_app_primary_url(self) -> str | None:
        """Deep link for in-app list (e.g. manager application when reviewers must be assigned)."""
        if self.verb != Notification.Verb.REVIEW_INTAKE_MANAGER:
            return None
        if not self.content_type_id or not self.object_id:
            return None
        model = self.content_type.model_class()
        if model is None or model._meta.label_lower != "rims_grants.application":
            return None
        return reverse("rims_grants:manager_application_detail", kwargs={"pk": self.object_id})


class NotificationPreference(models.Model):
    class Channel(models.TextChoices):
        EMAIL = "email", "Email"
        SMS = "sms", "SMS"
        IN_APP = "in_app", "In app"

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="notification_preferences",
    )
    channel = models.CharField(max_length=16, choices=Channel.choices)
    verb = models.CharField(max_length=64, blank=True, help_text="Empty = all verbs for channel")
    enabled = models.BooleanField(default=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["user", "channel", "verb"],
                name="uniq_notification_pref_user_channel_verb",
            ),
        ]


class NotificationTemplate(models.Model):
    """Per-verb email content override.

    When a row exists for a verb and is_active=True, ``send_notification``
    renders subject / body_text / body_html using Django's template engine
    against the ``email_context`` plus the ``notification`` and ``recipient``
    objects. When no row exists (or it is inactive) ``send_notification``
    falls back to the previous behaviour (caller-supplied subject + generic
    HTML wrapper).

    The model is exposed via the operations admin UI so non-developers can
    edit copy without a code change. Configuration enforcement that the PRD
    needs to be admin-driven (e.g. FRFA-AM038 reviewer-comment toggle in the
    decision email) is implemented by referencing context flags inside the
    template body rather than by branching in code.
    """

    verb = models.CharField(
        max_length=64,
        unique=True,
        choices=Notification.Verb.choices,
        help_text="Notification verb this template applies to.",
    )
    name = models.CharField(
        max_length=120,
        help_text="Human-readable label shown in the admin UI.",
    )
    subject = models.CharField(
        max_length=255,
        help_text="Email subject. Django template syntax (e.g. {{ recipient_name }}) is supported.",
    )
    body_text = models.TextField(
        blank=True,
        help_text="Plain-text body. Falls back to message if empty.",
    )
    body_html = models.TextField(
        blank=True,
        help_text="Optional HTML body. Wrapped by the generic email shell when set.",
    )
    action_label_default = models.CharField(
        max_length=120,
        blank=True,
        default="Open in IILMP",
        help_text="Default CTA label, used when email_context does not provide one.",
    )
    headline_default = models.CharField(
        max_length=255,
        blank=True,
        help_text="Default headline shown above the body in the HTML shell.",
    )
    is_active = models.BooleanField(default=True)
    updated_at = models.DateTimeField(auto_now=True)
    updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    class Meta:
        ordering = ["verb"]

    def __str__(self) -> str:  # pragma: no cover - admin display only
        return f"{self.get_verb_display()} ({'active' if self.is_active else 'inactive'})"
