import random
import re
import string
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password
from django.db import models
from django.utils import timezone


def generate_uid():
    """A unique random 6-digit numeric UID (100000–999999), unique across both
    users (Profile) and agents (Agent)."""
    while True:
        uid = str(random.randint(100000, 999999))
        if (not Profile.objects.filter(uid=uid).exists()
                and not Agent.objects.filter(uid=uid).exists()):
            return uid


def generate_referral_code():
    """A unique 6-character alphanumeric referral code, unique across both
    users (Profile) and agents (Agent)."""
    chars = string.ascii_uppercase + string.digits
    while True:
        code = ''.join(random.choices(chars, k=6))
        if (not Profile.objects.filter(referral_code=code).exists()
                and not Agent.objects.filter(referral_code=code).exists()):
            return code


class Agent(models.Model):
    """A management agent, registered by an admin. Each agent has a unique UID
    and referral code; sharing ``/register/?ref=<code>`` groups every user who
    registers through it under this agent."""
    name = models.CharField(max_length=128)
    AGENT = 'agent'
    ADMIN = 'admin'
    SUPER_ADMIN = 'super_admin'
    ROLE_CHOICES = [
        (AGENT, 'Agent'),
        (ADMIN, 'Admin'),
        (SUPER_ADMIN, 'Super admin'),
    ]

    role = models.CharField(max_length=16, choices=ROLE_CHOICES, default=AGENT)
    email = models.EmailField(blank=True)
    uid = models.CharField(max_length=6, unique=True, blank=True)
    referral_code = models.CharField(max_length=6, unique=True, blank=True)
    # Credentials for the agent portal (separate from end-user accounts).
    username = models.CharField(max_length=64, unique=True, null=True, blank=True)
    password = models.CharField(max_length=128, blank=True)  # hashed
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['name']

    def save(self, *args, **kwargs):
        if not self.uid:
            self.uid = generate_uid()
        if not self.referral_code:
            self.referral_code = generate_referral_code()
        super().save(*args, **kwargs)

    def __str__(self):
        return f'{self.name} ({self.referral_code})'

    def set_password(self, raw_password):
        self.password = make_password(raw_password)

    def check_password(self, raw_password):
        return bool(self.password) and check_password(raw_password, self.password)

    @property
    def has_login(self):
        """True when this agent has portal credentials set."""
        return bool(self.username and self.password)

    @property
    def registration_path(self):
        """Relative registration link carrying this agent's referral code."""
        return f'/register/?ref={self.referral_code}'

    @property
    def user_count(self):
        return self.members.count()


class Profile(models.Model):
    """Extra per-user data captured at registration."""
    ACTIVE = 'active'
    FROZEN = 'frozen'
    BANNED = 'banned'
    STATUS_CHOICES = [
        (ACTIVE, 'Active'),
        (FROZEN, 'Frozen'),
        (BANNED, 'Banned'),
    ]

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='profile',
    )
    uid = models.CharField(max_length=6, unique=True, blank=True)
    phone = models.CharField(max_length=32, blank=True)
    invitation_code = models.CharField(max_length=32, blank=True)
    # The user's own unique referral code (6-char alphanumeric) and the agent
    # whose code they registered under (every user is grouped under an agent).
    referral_code = models.CharField(max_length=6, unique=True, null=True, blank=True)
    agent = models.ForeignKey(
        'Agent', null=True, blank=True,
        on_delete=models.SET_NULL, related_name='members',
    )
    # The agent who approved/unfroze this user for login (if an agent did it).
    activated_by = models.ForeignKey(
        'Agent', null=True, blank=True,
        on_delete=models.SET_NULL, related_name='activated_members',
    )
    activated_at = models.DateTimeField(null=True, blank=True)
    # Account control state set by an admin. FROZEN is a temporary block,
    # BANNED is permanent; both prevent login (kept in sync with is_active).
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=ACTIVE)
    # The highest set (of 32 tickets) the user is approved to work on. Everyone
    # starts at set 1; an admin must "reset" (approve) the user to advance to the
    # next set after each set is completed.
    approved_set = models.PositiveIntegerField(default=1)
    # When True, this user bypasses the free 32-ticket trial round: completed
    # tickets count toward the 3-set game immediately (set by the admin "reset"
    # action). Normal users keep the trial offset.
    skip_trial = models.BooleanField(default=False)
    # Admin override of the user's completed-ticket count. When set (not None) it
    # replaces the live count derived from bookings, so an admin can directly set
    # how many tickets the user has "booked" (and therefore which set they sit in).
    # Leave blank to fall back to the live count.
    tickets_override = models.PositiveIntegerField(null=True, blank=True)
    # Admin wallet display overrides. Leave blank to use live totals derived from
    # transactions and bookings.
    balance_override = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    bonus_override = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    processing_override = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    # Admin override of the user's wallet "Earnings" (total commission). When set
    # (not None) it replaces the live commission sum shown in the wallet.
    earnings_override = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
    # Admin override allowing the user to withdraw commission regardless of their
    # set/ticket progress (i.e. without having to adjust the number of sets).
    withdraw_unlocked = models.BooleanField(default=False)
    # Admin lock used while tax / government fee processing is pending. When
    # enabled, the user cannot submit a withdrawal even if progress is complete.
    withdraw_locked = models.BooleanField(default=False)
    withdraw_lock_reason = models.CharField(max_length=255, blank=True)
    # A 4-digit withdrawal PIN the user sets themselves (stored hashed). When set,
    # it must be entered to confirm a withdrawal.
    withdrawal_pin = models.CharField(max_length=128, blank=True)
    # Presence tracking (updated by LastSeenMiddleware on each request).
    last_seen = models.DateTimeField(null=True, blank=True)
    last_ip = models.GenericIPAddressField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.uid:
            self.uid = generate_uid()
        if not self.referral_code:
            self.referral_code = generate_referral_code()
        super().save(*args, **kwargs)

    def __str__(self):
        return f'Profile<{self.user.username}> ({self.uid})'

    @property
    def is_online(self):
        """True when the user was seen within the last 5 minutes."""
        if not self.last_seen:
            return False
        return (timezone.now() - self.last_seen) <= timedelta(minutes=5)

    def set_withdrawal_pin(self, raw_pin):
        """Store the 4-digit withdrawal PIN as a hash."""
        self.withdrawal_pin = make_password(raw_pin)

    def check_withdrawal_pin(self, raw_pin):
        """True when ``raw_pin`` matches the stored withdrawal PIN."""
        return bool(self.withdrawal_pin) and check_password(raw_pin, self.withdrawal_pin)

    @property
    def has_withdrawal_pin(self):
        """True once the user has set a withdrawal PIN."""
        return bool(self.withdrawal_pin)


class WalletBinding(models.Model):
    """A payout account a user has bound to their account.

    A user may bind one account per network/method (ETH, BTC, USDT, Revolut,
    IBAN). Used as the payout destination for withdrawals and shown on the
    deposit screen. Re-binding the same network updates its row.
    """
    ETH = 'eth'
    BTC = 'btc'
    ERC20 = 'erc20'
    TRC20 = 'trc20'
    REVOLUT = 'revolut'
    IBAN = 'iban'
    NETWORK_CHOICES = [
        (ETH, 'ETHEREUM ETH'),
        (BTC, 'BITCOIN BTC'),
        (ERC20, 'USDT ERC20'),
        (TRC20, 'USDT TRC20'),
        (REVOLUT, 'Revolut'),
        (IBAN, 'IBAN'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='wallets',
    )
    network = models.CharField(max_length=16, choices=NETWORK_CHOICES)
    address = models.CharField(max_length=128)
    # Detailed account info collected for bank-style payout methods (IBAN /
    # Revolut). Left blank for crypto networks, which only use ``address``.
    account_name = models.CharField(max_length=128, blank=True)
    account_number = models.CharField(max_length=64, blank=True)
    contact_number = models.CharField(max_length=32, blank=True)
    email = models.CharField(max_length=254, blank=True)
    date_of_birth = models.CharField(max_length=32, blank=True)
    revtag = models.CharField(max_length=64, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        # One binding per user per network.
        unique_together = ('user', 'network')

    def __str__(self):
        return f'{self.get_network_display()}: {self.address}'

    @property
    def network_label(self):
        return self.get_network_display()

    @property
    def masked_address(self):
        """Show the first few characters then mask the rest, e.g. 0x24e********."""
        a = self.address or ''
        if len(a) <= 5:
            return a
        return a[:5] + '********'


class WithdrawalRequest(models.Model):
    """A withdrawal submitted by a user, pending admin approval.

    Every withdrawal is recorded here so an administrator can review and
    approve ("prove") it before payout.
    """
    PENDING = 'pending'
    APPROVED = 'approved'
    REJECTED = 'rejected'
    STATUS_CHOICES = [
        (PENDING, 'Pending'),
        (APPROVED, 'Approved'),
        (REJECTED, 'Rejected'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='withdrawals',
    )
    method = models.CharField(max_length=16, choices=WalletBinding.NETWORK_CHOICES)
    address = models.CharField(max_length=128)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    service_charge = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=PENDING)
    created_at = models.DateTimeField(auto_now_add=True)
    processed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.user.username} {self.amount} ({self.get_status_display()})'

    @property
    def method_label(self):
        return self.get_method_display()

    @property
    def amount_received(self):
        return self.amount - (self.service_charge or 0)


class Transaction(models.Model):
    """A single wallet movement shown in Account Details / history."""
    DEPOSIT = 'deposit'
    TICKET_PRINCIPAL = 'ticket_principal'
    TICKET_PRINCIPAL_RETURN = 'ticket_principal_return'
    COMMISSION = 'commission'
    WITHDRAWAL = 'withdrawal'
    ADJUSTMENT = 'adjustment'
    WELCOME_BONUS = 'welcome_bonus'
    BONUS_REVERSAL = 'bonus_reversal'
    KIND_CHOICES = [
        (DEPOSIT, 'Deposit'),
        (TICKET_PRINCIPAL, 'Ticket Principal'),
        (TICKET_PRINCIPAL_RETURN, 'Ticket Principal Return'),
        (COMMISSION, 'Receive a commission'),
        (WITHDRAWAL, 'Withdrawal'),
        (ADJUSTMENT, 'Admin Adjustment'),
        (WELCOME_BONUS, 'Welcome Bonus'),
        (BONUS_REVERSAL, 'Welcome Bonus Expired'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='transactions',
    )
    kind = models.CharField(max_length=32, choices=KIND_CHOICES)
    # Signed: ticket principal is negative, returns/commission positive.
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    created_at = models.DateTimeField()

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.get_kind_display()} {self.amount} ({self.user.username})'

    @property
    def label(self):
        return self.get_kind_display()

    @property
    def is_debit(self):
        """Principal deductions and withdrawals render green; returns/commission render orange."""
        return self.kind in (self.TICKET_PRINCIPAL, self.WITHDRAWAL, self.BONUS_REVERSAL)

class MovieRating(models.Model):
    """Rating of a movie submitted by a user."""
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='movie_ratings',
    )
    movie_id = models.CharField(max_length=64) # TVMaze ID or similar
    movie_title = models.CharField(max_length=255)
    stars = models.PositiveSmallIntegerField() # 1 to 5
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.user.username} rated {self.movie_title}: {self.stars} stars'


class TicketBooking(models.Model):
    """Record of a user booking/ticketing from the task part.

    A booking is created in PROCESSING state when the user rates a movie (the
    principal is deducted then), and moves to COMPLETED when they submit (the
    principal is returned plus commission).
    """
    PROCESSING = 'processing'
    COMPLETED = 'completed'
    STATUS_CHOICES = [
        (PROCESSING, 'Processing'),
        (COMPLETED, 'Completed'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='ticket_bookings',
    )
    movie_id = models.CharField(max_length=64)
    movie_title = models.CharField(max_length=255)
    amount = models.DecimalField(max_digits=12, decimal_places=2) # Ticket price (principal)
    commission = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=PROCESSING)
    # True when this booking used an admin-set special ticket (any kind);
    # ``special_kind`` records which kind it was for display.
    is_premium = models.BooleanField(default=False)
    special_kind = models.CharField(max_length=16, blank=True, default='')
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.user.username} booked {self.movie_title} ({self.get_status_display()})'

    @property
    def total_return(self):
        """Principal returned plus commission, paid out on completion."""
        return (self.amount or 0) + (self.commission or 0)

    @property
    def special_kind_label(self):
        """Human label for the admin-set ticket kind, e.g. 'Golden Egg'.
        Falls back to 'Premium' for legacy rows that predate ``special_kind``."""
        if not self.is_premium:
            return ''
        return dict(PremiumTicket.KIND_CHOICES).get(self.special_kind, 'Premium')


class DepositAddress(models.Model):
    """Admin-managed receiving address + QR code for a deposit method.

    The address/QR are "temporary" — an admin can update them at any time and
    the change is reflected on the Crypto Pay screen immediately.
    """
    method = models.CharField(max_length=32, unique=True)
    label = models.CharField(max_length=64, blank=True)
    address = models.CharField(max_length=255, blank=True)
    qr_code = models.FileField(upload_to='deposit_qr/', blank=True, null=True)
    is_active = models.BooleanField(default=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f'{self.label or self.method}: {self.address}'


class DepositRequest(models.Model):
    """A deposit submitted by a user with a payment-proof screenshot, pending
    admin verification. The balance is credited only when an admin approves."""
    PENDING = 'pending'
    APPROVED = 'approved'
    REJECTED = 'rejected'
    STATUS_CHOICES = [
        (PENDING, 'Pending'),
        (APPROVED, 'Approved'),
        (REJECTED, 'Rejected'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='deposit_requests',
    )
    method = models.CharField(max_length=32)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    proof = models.FileField(upload_to='deposit_proofs/')
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=PENDING)
    created_at = models.DateTimeField(auto_now_add=True)
    processed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.user.username} {self.amount} ({self.get_status_display()})'

    @property
    def uid(self):
        profile = getattr(self.user, 'profile', None)
        return profile.uid if profile else ''


class PremiumTicket(models.Model):
    """An admin-configured special ticket granted to a user for one set.

    Covers all three admin-set ticket kinds (Premium, Golden Egg, Special) —
    they behave identically and differ only in name/branding (see ``kind``).
    When set, it takes effect on a specific ticket within the targeted set: the
    ticket the user reserves at position ``ticket_number`` (1–32) takes this
    ``amount`` (price) and ``commission`` (payout) instead of the normal
    balance-based pricing. Leave ``ticket_number`` blank to apply on the very
    next ticket the user reserves in that set. Consumed (``is_used``) once that
    booking completes, so it applies at most once per set.
    """
    PREMIUM = 'premium'
    GOLDEN_EGG = 'golden_egg'
    SPECIAL = 'special'
    KIND_CHOICES = [
        (PREMIUM, 'Premium Ticket'),
        (GOLDEN_EGG, 'Golden Egg'),
        (SPECIAL, 'Special Ticket'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='premium_tickets',
    )
    kind = models.CharField(max_length=16, choices=KIND_CHOICES, default=PREMIUM)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    commission = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    # The 1-based set this ticket applies to (auto-filled to the user's current
    # set when left blank in the admin).
    set_number = models.PositiveIntegerField(null=True, blank=True)
    # The 1-based ticket position within that set (1–32) at which this special
    # ticket appears. Leave blank to apply on the next ticket the user reserves.
    ticket_number = models.PositiveIntegerField(null=True, blank=True)
    is_used = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    used_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        state = 'used' if self.is_used else 'pending'
        return f'{self.get_kind_display()} {self.amount} for {self.user.username} (set {self.set_number}, {state})'


class GoldenEggTicket(PremiumTicket):
    """Proxy of :class:`PremiumTicket` giving Golden Eggs their own admin section."""
    class Meta:
        proxy = True
        verbose_name = 'Golden egg'
        verbose_name_plural = 'Golden eggs'


class SpecialTicket(PremiumTicket):
    """Proxy of :class:`PremiumTicket` giving Special Tickets their own admin section."""
    class Meta:
        proxy = True
        verbose_name = 'Special ticket'
        verbose_name_plural = 'Special tickets'


class SetApprovalRequest(models.Model):
    """Raised when a user finishes a set (32 tickets) and needs admin approval
    to proceed to the next one. Serves as the admin's notification; approving it
    ("reset user") bumps the user's Profile.approved_set."""
    PENDING = 'pending'
    APPROVED = 'approved'
    STATUS_CHOICES = [
        (PENDING, 'Pending'),
        (APPROVED, 'Approved'),
    ]

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='set_approvals',
    )
    set_completed = models.PositiveIntegerField()
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=PENDING)
    created_at = models.DateTimeField(auto_now_add=True)
    processed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.user.username} finished set {self.set_completed} ({self.get_status_display()})'

    @property
    def uid(self):
        profile = getattr(self.user, 'profile', None)
        return profile.uid if profile else ''


class FeaturedVideo(models.Model):
    """The "now playing" movie shown on the dashboard hero, managed by an admin.

    Upload a video file (e.g. MP4) or paste a YouTube link; the most recently
    updated active row is used. If none is set, the dashboard falls back to the
    first movie from the live listings API.
    """
    title = models.CharField(max_length=255, default='Now Playing')
    youtube_url = models.URLField(
        blank=True,
        help_text='Paste a YouTube link (watch, share, or embed). Leave blank if uploading a file.')
    video_file = models.FileField(
        upload_to='featured_videos/', blank=True, null=True,
        help_text='Optional: upload a video (e.g. MP4) to play instead of a YouTube link.')
    poster = models.FileField(
        upload_to='featured_posters/', blank=True, null=True,
        help_text='Optional poster image shown before the video plays.')
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-updated_at']

    def __str__(self):
        return self.title or 'Featured video'

    @property
    def is_youtube(self):
        return not self.video_file and bool(self.youtube_url)

    @property
    def play_url(self):
        """A directly playable URL: the uploaded file, or a normalised YouTube
        embed URL."""
        if self.video_file:
            return self.video_file.url
        url = (self.youtube_url or '').strip()
        if not url:
            return ''
        match = re.search(
            r'(?:youtu\.be/|youtube\.com/(?:watch\?v=|embed/|shorts/|v/))([\w-]{11})', url)
        return 'https://www.youtube.com/embed/' + match.group(1) if match else url

    @property
    def poster_url(self):
        return self.poster.url if self.poster else ''
