"""
Public API Models

This module contains models for managing API tokens and webhooks for the
public API v1. Business owners can create API tokens with specific scopes
to allow third-party integrations to access their data.
"""

import hashlib
import secrets
import uuid
from django.conf import settings
from django.db import models
from django.utils import timezone


class APIScope:
    """
    Available API scopes for token permissions.

    Scopes follow the format: resource:action
    - read: allows GET requests
    - write: allows POST, PATCH, DELETE requests
    - manage: allows full CRUD operations
    """
    SERVICES_READ = 'services:read'
    RESOURCES_READ = 'resources:read'
    AVAILABILITY_READ = 'availability:read'
    BOOKINGS_READ = 'bookings:read'
    BOOKINGS_WRITE = 'bookings:write'
    CUSTOMERS_READ = 'customers:read'
    CUSTOMERS_WRITE = 'customers:write'
    BUSINESS_READ = 'business:read'
    WEBHOOKS_MANAGE = 'webhooks:manage'

    CHOICES = [
        (SERVICES_READ, 'View services and pricing'),
        (RESOURCES_READ, 'View resources and staff'),
        (AVAILABILITY_READ, 'Check time slot availability'),
        (BOOKINGS_READ, 'View appointments'),
        (BOOKINGS_WRITE, 'Create, update, and cancel appointments'),
        (CUSTOMERS_READ, 'View customer information'),
        (CUSTOMERS_WRITE, 'Create and update customers'),
        (BUSINESS_READ, 'View business information'),
        (WEBHOOKS_MANAGE, 'Manage webhook subscriptions'),
    ]

    ALL_SCOPES = [choice[0] for choice in CHOICES]

    # Scope groupings for common use cases
    BOOKING_WIDGET_SCOPES = [
        SERVICES_READ,
        RESOURCES_READ,
        AVAILABILITY_READ,
        BOOKINGS_WRITE,
        CUSTOMERS_WRITE,
    ]

    BUSINESS_DIRECTORY_SCOPES = [
        BUSINESS_READ,
        SERVICES_READ,
        RESOURCES_READ,
    ]

    APPOINTMENT_DASHBOARD_SCOPES = [
        BOOKINGS_READ,
        BOOKINGS_WRITE,
        CUSTOMERS_READ,
    ]

    CUSTOMER_SELF_SERVICE_SCOPES = [
        BOOKINGS_READ,
        BOOKINGS_WRITE,
        AVAILABILITY_READ,
    ]

    FULL_INTEGRATION_SCOPES = ALL_SCOPES


class APIToken(models.Model):
    """
    API Token for authenticating third-party integrations.

    Tokens are generated with a secure random key. The full key is only
    shown once during creation - we store a hash for verification.

    Token format: ss_live_<32 random hex chars>
    Example: ss_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

    Attributes:
        id: UUID primary key
        tenant: The business this token belongs to
        name: Human-readable name for the token (e.g., "Website Integration")
        key_hash: SHA-256 hash of the full token key
        key_prefix: First 8 characters of the key for identification
        scopes: List of permission scopes granted to this token
        is_active: Whether the token is currently active
        created_at: When the token was created
        last_used_at: When the token was last used for authentication
        expires_at: Optional expiration date
        created_by: User who created this token
        rate_limit_override: Custom rate limit (requests/hour) if set
    """

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    tenant = models.ForeignKey(
        'core.Tenant',
        on_delete=models.CASCADE,
        related_name='api_tokens',
        help_text='The business this token belongs to'
    )
    name = models.CharField(
        max_length=100,
        help_text='Human-readable name for identifying this token'
    )
    key_hash = models.CharField(
        max_length=64,
        unique=True,
        db_index=True,
        help_text='SHA-256 hash of the token key'
    )
    key_prefix = models.CharField(
        max_length=16,
        help_text='Prefix of the key for identification (e.g., ss_live_a1b2)'
    )
    plaintext_key = models.CharField(
        max_length=72,
        null=True,
        blank=True,
        help_text='ONLY for sandbox tokens: stores the full key for documentation. NEVER set for live tokens!'
    )
    scopes = models.JSONField(
        default=list,
        help_text='List of permission scopes granted to this token'
    )
    is_active = models.BooleanField(
        default=True,
        help_text='Whether this token is currently active'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    last_used_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When the token was last used for authentication'
    )
    expires_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='Optional expiration date for the token'
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='created_api_tokens',
        help_text='User who created this token'
    )
    rate_limit_override = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text='Custom rate limit (requests/hour) if different from default'
    )

    # Sandbox/Test mode
    is_sandbox = models.BooleanField(
        default=False,
        help_text='Whether this is a sandbox/test token (uses test data)'
    )

    class Meta:
        app_label = 'public_api'
        verbose_name = 'API Token'
        verbose_name_plural = 'API Tokens'
        ordering = ['-created_at']

    def __str__(self):
        return f"{self.name} ({self.key_prefix}...)"

    def clean(self):
        """
        Validate the model to enforce security rules.

        CRITICAL SECURITY CHECKS:
        1. NEVER allow plaintext_key for live tokens (is_sandbox=False)
        2. NEVER allow plaintext_key that starts with ss_live_*
        """
        from django.core.exceptions import ValidationError

        if self.plaintext_key:
            # SECURITY: Never allow plaintext storage for live tokens
            if not self.is_sandbox:
                raise ValidationError({
                    'plaintext_key': 'SECURITY VIOLATION: Cannot store plaintext key for live/production tokens. '
                                   'Only sandbox tokens may store plaintext keys for documentation purposes.'
                })

            # SECURITY: Double-check the plaintext key doesn't start with ss_live_
            if self.plaintext_key.startswith('ss_live_'):
                raise ValidationError({
                    'plaintext_key': 'SECURITY VIOLATION: Plaintext key appears to be a live token (ss_live_*). '
                                   'Only sandbox tokens (ss_test_*) may be stored in plaintext.'
                })

            # SECURITY: Verify it's actually a test token
            if not self.plaintext_key.startswith('ss_test_'):
                raise ValidationError({
                    'plaintext_key': 'Invalid plaintext key format. Must start with ss_test_'
                })

    def save(self, *args, **kwargs):
        """Override save to always run validation."""
        self.full_clean()  # Always validate before saving
        super().save(*args, **kwargs)

    @classmethod
    def generate_key(cls, is_sandbox=False):
        """
        Generate a new secure API key.

        Args:
            is_sandbox: If True, generates a test token (ss_test_*), otherwise live (ss_live_*)

        Returns:
            tuple: (full_key, key_hash, key_prefix)
            - full_key: The complete token to show to the user once
            - key_hash: SHA-256 hash to store in database
            - key_prefix: First characters for identification
        """
        random_part = secrets.token_hex(32)
        prefix = "ss_test_" if is_sandbox else "ss_live_"
        full_key = f"{prefix}{random_part}"
        key_hash = hashlib.sha256(full_key.encode()).hexdigest()
        key_prefix = full_key[:16]  # "ss_live_" or "ss_test_" + first 8 hex chars
        return full_key, key_hash, key_prefix

    @classmethod
    def is_sandbox_key(cls, key):
        """Check if an API key is a sandbox/test key based on its prefix."""
        return key.startswith('ss_test_')

    @classmethod
    def hash_key(cls, key):
        """Hash a key for comparison."""
        return hashlib.sha256(key.encode()).hexdigest()

    @classmethod
    def get_by_key(cls, key):
        """
        Retrieve a token by its full key.

        Args:
            key: The full API key string

        Returns:
            APIToken or None
        """
        key_hash = cls.hash_key(key)
        try:
            return cls.objects.select_related('tenant').get(
                key_hash=key_hash,
                is_active=True
            )
        except cls.DoesNotExist:
            return None

    def has_scope(self, scope):
        """Check if this token has a specific scope."""
        return scope in self.scopes

    def has_any_scope(self, scopes):
        """Check if this token has any of the specified scopes."""
        return any(scope in self.scopes for scope in scopes)

    def has_all_scopes(self, scopes):
        """Check if this token has all of the specified scopes."""
        return all(scope in self.scopes for scope in scopes)

    def is_expired(self):
        """Check if the token has expired."""
        if self.expires_at is None:
            return False
        return timezone.now() > self.expires_at

    def is_valid(self):
        """Check if the token is valid (active and not expired)."""
        return self.is_active and not self.is_expired()

    def update_last_used(self):
        """Update the last_used_at timestamp."""
        self.last_used_at = timezone.now()
        self.save(update_fields=['last_used_at'])


class WebhookEvent:
    """
    Available webhook event types.

    Events are named as: resource.action
    """
    APPOINTMENT_CREATED = 'appointment.created'
    APPOINTMENT_UPDATED = 'appointment.updated'
    APPOINTMENT_CANCELLED = 'appointment.cancelled'
    APPOINTMENT_COMPLETED = 'appointment.completed'
    APPOINTMENT_REMINDER = 'appointment.reminder'
    CUSTOMER_CREATED = 'customer.created'
    CUSTOMER_UPDATED = 'customer.updated'
    PAYMENT_SUCCEEDED = 'payment.succeeded'
    PAYMENT_FAILED = 'payment.failed'

    CHOICES = [
        (APPOINTMENT_CREATED, 'Appointment Created'),
        (APPOINTMENT_UPDATED, 'Appointment Updated'),
        (APPOINTMENT_CANCELLED, 'Appointment Cancelled'),
        (APPOINTMENT_COMPLETED, 'Appointment Completed'),
        (APPOINTMENT_REMINDER, 'Appointment Reminder (24h before)'),
        (CUSTOMER_CREATED, 'Customer Created'),
        (CUSTOMER_UPDATED, 'Customer Updated'),
        (PAYMENT_SUCCEEDED, 'Payment Succeeded'),
        (PAYMENT_FAILED, 'Payment Failed'),
    ]

    ALL_EVENTS = [choice[0] for choice in CHOICES]


class WebhookSubscription(models.Model):
    """
    Webhook subscription for receiving real-time event notifications.

    When events occur (e.g., appointment created), we send a POST request
    to the subscription URL with the event data. The payload is signed
    with HMAC-SHA256 using the subscription's secret.

    Attributes:
        id: UUID primary key
        tenant: The business this webhook belongs to
        api_token: The API token that created/owns this subscription
        url: The HTTPS URL to send webhook payloads to
        secret: Secret key for HMAC-SHA256 signature verification
        events: List of event types to subscribe to
        is_active: Whether the subscription is currently active
        created_at: When the subscription was created
        failure_count: Number of consecutive delivery failures
        last_triggered_at: When a webhook was last sent
        last_success_at: When a webhook was last successfully delivered
        last_failure_at: When a webhook last failed to deliver
        description: Optional description of what this webhook is for
    """

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    tenant = models.ForeignKey(
        'core.Tenant',
        on_delete=models.CASCADE,
        related_name='webhook_subscriptions',
        help_text='The business this webhook belongs to'
    )
    api_token = models.ForeignKey(
        APIToken,
        on_delete=models.CASCADE,
        related_name='webhook_subscriptions',
        help_text='The API token that owns this subscription'
    )
    url = models.URLField(
        max_length=2048,
        help_text='The HTTPS URL to send webhook payloads to'
    )
    secret = models.CharField(
        max_length=64,
        help_text='Secret key for HMAC-SHA256 signature verification'
    )
    events = models.JSONField(
        default=list,
        help_text='List of event types to subscribe to'
    )
    is_active = models.BooleanField(
        default=True,
        help_text='Whether this subscription is currently active'
    )
    created_at = models.DateTimeField(auto_now_add=True)

    # Health tracking
    failure_count = models.PositiveIntegerField(
        default=0,
        help_text='Number of consecutive delivery failures'
    )
    last_triggered_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When a webhook was last sent'
    )
    last_success_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When a webhook was last successfully delivered'
    )
    last_failure_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When a webhook last failed to deliver'
    )
    description = models.TextField(
        blank=True,
        help_text='Optional description of what this webhook is for'
    )

    # Auto-disable after too many failures
    MAX_CONSECUTIVE_FAILURES = 10

    class Meta:
        app_label = 'public_api'
        verbose_name = 'Webhook Subscription'
        verbose_name_plural = 'Webhook Subscriptions'
        ordering = ['-created_at']

    def __str__(self):
        return f"Webhook to {self.url} ({len(self.events)} events)"

    @classmethod
    def generate_secret(cls):
        """Generate a secure webhook secret."""
        return secrets.token_hex(32)

    def is_subscribed_to(self, event_type):
        """Check if this subscription should receive the given event type."""
        return event_type in self.events

    def record_success(self):
        """Record a successful delivery."""
        self.failure_count = 0
        self.last_success_at = timezone.now()
        self.last_triggered_at = timezone.now()
        self.save(update_fields=['failure_count', 'last_success_at', 'last_triggered_at'])

    def record_failure(self):
        """
        Record a failed delivery.

        If consecutive failures exceed MAX_CONSECUTIVE_FAILURES,
        the subscription is automatically disabled.
        """
        self.failure_count += 1
        self.last_failure_at = timezone.now()
        self.last_triggered_at = timezone.now()

        if self.failure_count >= self.MAX_CONSECUTIVE_FAILURES:
            self.is_active = False

        self.save(update_fields=['failure_count', 'last_failure_at', 'last_triggered_at', 'is_active'])


class WebhookDelivery(models.Model):
    """
    Record of a webhook delivery attempt.

    Each time we attempt to deliver a webhook, we create a record here
    with the payload sent, response received, and delivery status.

    Attributes:
        id: UUID primary key
        subscription: The webhook subscription this delivery is for
        event_type: The type of event (e.g., 'appointment.created')
        event_id: Unique identifier for this event instance
        payload: The JSON payload that was/will be sent
        response_status: HTTP status code received (null if not yet delivered)
        response_body: Response body text (truncated to 10KB)
        delivered_at: When the webhook was successfully delivered
        created_at: When this delivery record was created
        success: Whether the delivery was successful
        retry_count: Number of retry attempts made
        next_retry_at: When the next retry will be attempted
        error_message: Error message if delivery failed
    """

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    subscription = models.ForeignKey(
        WebhookSubscription,
        on_delete=models.CASCADE,
        related_name='deliveries',
        help_text='The subscription this delivery is for'
    )
    event_type = models.CharField(
        max_length=50,
        help_text='The type of event'
    )
    event_id = models.CharField(
        max_length=64,
        help_text='Unique identifier for this event instance'
    )
    payload = models.JSONField(
        help_text='The JSON payload sent to the webhook URL'
    )
    response_status = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text='HTTP status code received'
    )
    response_body = models.TextField(
        blank=True,
        help_text='Response body (truncated to 10KB)'
    )
    delivered_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When the webhook was successfully delivered'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    success = models.BooleanField(
        default=False,
        help_text='Whether the delivery was successful'
    )
    retry_count = models.PositiveIntegerField(
        default=0,
        help_text='Number of retry attempts made'
    )
    next_retry_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='When the next retry will be attempted'
    )
    error_message = models.TextField(
        blank=True,
        help_text='Error message if delivery failed'
    )

    MAX_RETRIES = 5
    # Retry delays in seconds: 1min, 5min, 30min, 2hr, 8hr
    RETRY_DELAYS = [60, 300, 1800, 7200, 28800]

    class Meta:
        app_label = 'public_api'
        verbose_name = 'Webhook Delivery'
        verbose_name_plural = 'Webhook Deliveries'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['subscription', '-created_at']),
            models.Index(fields=['event_type', '-created_at']),
            models.Index(fields=['success', 'next_retry_at']),
        ]

    def __str__(self):
        status = "Success" if self.success else f"Failed (retry {self.retry_count})"
        return f"{self.event_type} to {self.subscription.url} - {status}"

    def can_retry(self):
        """Check if this delivery can be retried."""
        return not self.success and self.retry_count < self.MAX_RETRIES

    def get_next_retry_delay(self):
        """Get the delay in seconds before the next retry."""
        if self.retry_count >= len(self.RETRY_DELAYS):
            return self.RETRY_DELAYS[-1]
        return self.RETRY_DELAYS[self.retry_count]

    def schedule_retry(self):
        """Schedule the next retry attempt."""
        if not self.can_retry():
            return False

        delay = self.get_next_retry_delay()
        self.next_retry_at = timezone.now() + timezone.timedelta(seconds=delay)
        self.save(update_fields=['next_retry_at'])
        return True

    def mark_success(self, status_code, response_body=''):
        """Mark this delivery as successful."""
        self.success = True
        self.response_status = status_code
        self.response_body = response_body[:10240]  # Truncate to 10KB
        self.delivered_at = timezone.now()
        self.next_retry_at = None
        self.save()
        self.subscription.record_success()

    def mark_failure(self, error_message, status_code=None, response_body=''):
        """Mark this delivery as failed and schedule retry if possible."""
        self.success = False
        self.response_status = status_code
        self.response_body = response_body[:10240]  # Truncate to 10KB
        self.error_message = error_message
        self.retry_count += 1
        self.save()

        if self.can_retry():
            self.schedule_retry()
        else:
            self.subscription.record_failure()
