from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.core.validators import EmailValidator
from django.utils import timezone
import uuid


def format_file_size(bytes_size):
    """Convert bytes to human readable format (KB, MB, GB, TB)"""
    if bytes_size == 0:
        return "0 B"
    size_units = ['B', 'KB', 'MB', 'GB', 'TB']
    size = float(bytes_size)
    unit_index = 0
    while size >= 1024 and unit_index < len(size_units) - 1:
        size /= 1024
        unit_index += 1
    return f"{size:.2f} {size_units[unit_index]}"


class Profile(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    user = models.OneToOneField(
        User, on_delete=models.CASCADE, related_name='profile', verbose_name=_("User")
    )
    email = models.EmailField(
        _("Email Address"), max_length=255, unique=True,
        validators=[EmailValidator()],
        help_text=_("Primary email address for the account")
    )
    agreed_to_terms = models.BooleanField(
        _("Agreed to Terms"), default=False,
        help_text=_("Whether the user agreed to terms and conditions")
    )
    terms_agreed_at = models.DateTimeField(
        _("Terms Agreed At"), null=True, blank=True,
        help_text=_("When the user agreed to the terms and conditions")
    )
    created_at = models.DateTimeField(_("Created At"), auto_now_add=True)
    updated_at = models.DateTimeField(_("Updated At"), auto_now=True)

    class Meta:
        verbose_name = _("Profile")
        verbose_name_plural = _("Profiles")
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['email']),
            models.Index(fields=['created_at']),
            models.Index(fields=['user', 'created_at']),
        ]

    def __str__(self):
        return f"{self.user.username} - {self.email}"

    def save(self, *args, **kwargs):
        if self.agreed_to_terms and not self.terms_agreed_at:
            self.terms_agreed_at = timezone.now()
        if self.user and self.user.email != self.email:
            self.user.email = self.email
            self.user.save()
        super().save(*args, **kwargs)

    @property
    def username(self):
        return self.user.username


class LocalUploadedFile(models.Model):
    """Store files uploaded from user's local computer via chunked upload."""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='local_uploads')
    transfer_order = models.ForeignKey(
        'TransferOrder', on_delete=models.SET_NULL,
        null=True, blank=True, related_name='local_files'
    )
    original_name = models.CharField(max_length=500)
    file_size = models.BigIntegerField(help_text="Size in bytes")
    file = models.FileField(upload_to='local_uploads/%Y/%m/%d/')

    total_chunks = models.IntegerField(default=1)
    received_chunks = models.IntegerField(default=0)
    upload_complete = models.BooleanField(default=False)

    uploaded_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-uploaded_at']

    def __str__(self):
        return f"{self.original_name} ({self.user.email})"

    @property
    def file_url(self):
        return self.file.url if self.file else None


class ChunkedUploadPart(models.Model):
    """Temporary storage for individual chunks during upload assembly."""
    upload_id = models.CharField(max_length=64, db_index=True)
    chunk_index = models.IntegerField()
    temp_path = models.CharField(max_length=500)
    size = models.BigIntegerField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['upload_id', 'chunk_index']
        ordering = ['chunk_index']

    def __str__(self):
        return f"Chunk {self.chunk_index} for upload {self.upload_id}"


class TransferOrder(models.Model):
    STATUS_CHOICES = [
        ('cloud_connected', 'Cloud Connected'),
        ('files_selected', 'Files Selected'),
        ('storage_selected', 'Storage Selected'),
        ('payment_completed', 'Payment Completed'),
        ('payment_pending', 'Payment Pending'),
        ('partially_paid', 'Partially Paid'),
        ('download_queued', 'Download Queued'),
        ('download_in_progress', 'Download In Progress'),
        ('download_completed', 'Download Completed'),
        ('download_failed', 'Download Failed'),
        ('download_partial', 'Download Partial'),
        ('processing', 'Processing Files'),
        ('shipped', 'Shipped'),
        ('delivered', 'Delivered'),
        ('completed', 'Completed'),
        ('cancelled', 'Cancelled'),
    ]

    CLOUD_SOURCE_CHOICES = [
        ('google_drive', 'Google Drive'),
        ('dropbox', 'Dropbox'),
        ('onedrive', 'OneDrive'),
        ('local_upload', 'Local Upload'),
    ]

    STORAGE_CHOICES = [
        ('flash_drive', 'USB Flash Drive'),
        ('usb', 'USB Flash Drive'),
        ('memory_card', 'Memory Card'),
        ('external_hdd', 'External HDD'),
        ('hdd', 'Hard Disk Drive'),
        ('ssd', 'Solid State Drive'),
    ]

    PAYMENT_PLAN_CHOICES = [
        ('full', 'Full Payment'),
        ('half', 'Pay Half Now'),
    ]

    # -------------------------------------------------------------------------
    # Shipping Zone Configuration
    # Adjust costs, county lists, labels, and ETAs here as needed.
    # All costs are in KES. Business base is Nairobi (Zone 1 = free).
    # -------------------------------------------------------------------------
    SHIPPING_ZONE_CONFIG = {
        'nairobi': {
            'name':        'Nairobi',
            'counties':    ['Nairobi'],
            'cost_kes':    0,
            'label':       'Free Delivery',
            'description': 'Business base — no shipping charge',
            'eta':         'Same day or next business day',
        },
        'metro': {
            'name':        'Nairobi Metro',
            'counties':    ['Kiambu', 'Kajiado', 'Machakos', 'Muranga'],
            'cost_kes':    250,
            'label':       'Metro Delivery',
            'description': 'Nairobi Metropolitan Area',
            'eta':         '1–2 business days',
        },
        'central': {
            'name':        'Central & Eastern',
            'counties':    [
                'Nakuru', 'Nyeri', 'Kirinyaga', 'Nyandarua', 'Laikipia',
                'Embu', 'Meru', 'Tharaka Nithi', 'Kitui', 'Makueni',
            ],
            'cost_kes':    400,
            'label':       'Central Kenya',
            'description': 'Central and Eastern Kenya',
            'eta':         '2–3 business days',
        },
        'rift_western': {
            'name':        'Rift Valley & Western',
            'counties':    [
                'Uasin Gishu', 'Nandi', 'Kericho', 'Bomet', 'Baringo',
                'Elgeyo Marakwet', 'Trans Nzoia', 'West Pokot', 'Narok',
                'Kakamega', 'Vihiga', 'Bungoma', 'Busia', 'Samburu',
            ],
            'cost_kes':    550,
            'label':       'Rift Valley & Western',
            'description': 'Rift Valley and Western Kenya',
            'eta':         '2–4 business days',
        },
        'nyanza': {
            'name':        'Nyanza',
            'counties':    ['Kisumu', 'Siaya', 'Homa Bay', 'Migori', 'Kisii', 'Nyamira'],
            'cost_kes':    600,
            'label':       'Nyanza Region',
            'description': 'Nyanza Region',
            'eta':         '3–4 business days',
        },
        'coast': {
            'name':        'Coast',
            'counties':    ['Mombasa', 'Kilifi', 'Kwale', 'Taita Taveta', 'Lamu'],
            'cost_kes':    700,
            'label':       'Coast Delivery',
            'description': 'Kenyan Coast',
            'eta':         '3–5 business days',
        },
        'remote': {
            'name':        'Remote / ASAL',
            'counties':    ['Turkana', 'Mandera', 'Wajir', 'Marsabit', 'Garissa', 'Tana River', 'Isiolo'],
            'cost_kes':    900,
            'label':       'Remote Delivery',
            'description': 'Arid and Semi-Arid Lands',
            'eta':         '5–7 business days',
        },
    }

    # -------------------------------------------------------------------------
    # Storage Pricing Table (KES)
    # Base prices are for USB/flash drives.  Device multipliers are applied on top.
    # Adjust base_prices_kes or device_factors to change pricing globally.
    # -------------------------------------------------------------------------
    STORAGE_PRICING = {
        # Base prices by capacity in GB → KES (matches storage selection display)
        'base_prices_kes': {
            64:    1_200,
            128:   2_200,
            256:   3_800,
            512:   6_500,
            1024:  9_500,
            2048: 15_000,
            4096: 28_000,
            5120: 35_000,
            8192: 52_000,
        },
        # Multiplier applied on top of base price per device type
        'device_factors': {
            'flash_drive':  1.00,
            'usb':          1.00,
            'memory_card':  1.10,
            'external_hdd': 1.50,
            'hdd':          1.50,
            'ssd':          2.80,
        },
        # Tax rate — change here to update VAT globally
        'tax_rate': '0.08',   # 8%
        # Minimum charge in KES
        'minimum_charge': 500,
    }

    # =========================================================================
    # Fields
    # =========================================================================
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    order_number = models.CharField(
        max_length=20, unique=True, editable=False, blank=True,
        help_text="Public order number for customer reference"
    )
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transfer_orders')
    cloud_source = models.CharField(max_length=20, choices=CLOUD_SOURCE_CHOICES, default='google_drive')

    # OAuth tokens
    access_token = models.TextField(null=True, blank=True, help_text="OAuth access token")
    refresh_token = models.TextField(null=True, blank=True, help_text="OAuth refresh token")
    token_expiry = models.DateTimeField(null=True, blank=True, help_text="When the access token expires")

    status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='cloud_connected')
    storage_type = models.CharField(max_length=50, choices=STORAGE_CHOICES, null=True, blank=True)
    storage_size = models.CharField(max_length=20, null=True, blank=True)
    shipping_address = models.TextField(null=True, blank=True)

    # Structured delivery fields
    county = models.CharField(max_length=100, null=True, blank=True)
    town = models.CharField(max_length=100, null=True, blank=True)
    street_address = models.CharField(max_length=255, null=True, blank=True)
    additional_delivery_info = models.TextField(null=True, blank=True)

    selected_files = models.JSONField(default=list, help_text="List of selected files with metadata")
    total_files = models.IntegerField(default=0)
    total_size = models.DecimalField(
        max_digits=12, decimal_places=4, default=0.0,
        help_text="Total size of selected files in GB"
    )

    # Payment
    payment_method = models.CharField(max_length=50, null=True, blank=True)
    payment_reference = models.CharField(
        max_length=120, null=True, blank=True,
        help_text="Paystack transaction reference"
    )
    payment_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
    payment_currency = models.CharField(max_length=3, default='KES')

    payment_plan = models.CharField(
        max_length=10, choices=PAYMENT_PLAN_CHOICES, default='full',
        help_text="Full payment or half now, balance on delivery"
    )
    amount_paid = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
    balance_due = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)

    # Download tracking
    download_started_at = models.DateTimeField(null=True, blank=True)
    download_completed_at = models.DateTimeField(null=True, blank=True)
    download_status = models.CharField(max_length=500, null=True, blank=True)
    download_log = models.JSONField(null=True, blank=True)
    retry_count = models.IntegerField(default=0)
    estimated_completion = models.DateTimeField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _("Transfer Order")
        verbose_name_plural = _("Transfer Orders")
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['user', 'status']),
            models.Index(fields=['status', 'created_at']),
            models.Index(fields=['order_number']),
            models.Index(fields=['created_at']),
            models.Index(fields=['payment_reference']),
        ]

    def __str__(self):
        return f"Order {self.order_number} - {self.user.email} - {self.status}"

    def save(self, *args, **kwargs):
        if not self.order_number:
            self.order_number = self._generate_order_number()
        if self.status in ['payment_completed', 'download_completed']:
            self.estimated_completion = timezone.now() + timezone.timedelta(hours=24)
        super().save(*args, **kwargs)

    def _generate_order_number(self):
        import random
        import string
        while True:
            chars = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
            order_num = f"TRANS-{chars}"
            if not TransferOrder.objects.filter(order_number=order_num).exists():
                return order_num

    # =========================================================================
    # Shipping
    # =========================================================================

    def get_shipping_zone_info(self):
        """
        Return the full zone config dict for this order's county.
        Tries structured fields first, then parses the legacy shipping_address.
        """
        county = (self.county or '').strip()

        # Fallback: parse county from legacy shipping_address string
        # Format stored by storage step: "street, town, district, county[, Postal Code: X, Kenya]"
        if not county and self.shipping_address:
            parts = [p.strip() for p in self.shipping_address.split(',')]
            all_counties = [
                c for zone in self.SHIPPING_ZONE_CONFIG.values()
                for c in zone['counties']
            ]
            # Index 3 is the expected position; also do a fallback scan
            candidate = parts[3] if len(parts) > 3 else ''
            if candidate in all_counties:
                county = candidate
            else:
                for part in parts:
                    if part in all_counties:
                        county = part
                        break

        if county:
            county_lower = county.strip().lower()
            for zone_key, zone_data in self.SHIPPING_ZONE_CONFIG.items():
                for c in zone_data['counties']:
                    if c.lower() == county_lower:
                        from decimal import Decimal
                        return {
                            'zone_key':    zone_key,
                            'county':      county,
                            'cost':        Decimal(str(zone_data['cost_kes'])),
                            **{k: v for k, v in zone_data.items() if k != 'cost_kes'},
                        }

        # Unknown county — use a safe default
        from decimal import Decimal
        return {
            'zone_key':    'unknown',
            'county':      county or 'Unknown',
            'name':        'Standard',
            'counties':    [],
            'cost':        Decimal('500.00'),
            'label':       'Standard Delivery',
            'description': 'Standard nationwide delivery',
            'eta':         '3–5 business days',
        }

    def calculate_shipping_cost(self):
        """Return shipping cost (KES Decimal) based on zone. Uses get_shipping_zone_info()."""
        return self.get_shipping_zone_info()['cost']

    # =========================================================================
    # Pricing
    # =========================================================================

    def _calculate_base_price(self):
        """
        Calculate base device price in KES from storage_type and storage_size.
        Uses STORAGE_PRICING config so adjustments stay in one place.
        """
        from decimal import Decimal

        pricing = self.STORAGE_PRICING
        base_prices = pricing['base_prices_kes']
        device_factors = pricing['device_factors']
        minimum = pricing['minimum_charge']

        # Parse storage size to GB
        size_gb = 64  # safe fallback
        if self.storage_size:
            try:
                size_str = self.storage_size.upper().strip()
                if 'TB' in size_str:
                    size_gb = int(float(size_str.replace('TB', '').strip()) * 1000)
                else:
                    size_gb = int(float(size_str.replace('GB', '').strip()))
            except (ValueError, AttributeError):
                size_gb = 64

        # Find the closest (at or above) capacity tier
        sorted_tiers = sorted(base_prices.keys())
        matched_tier = sorted_tiers[-1]
        for tier in sorted_tiers:
            if size_gb <= tier:
                matched_tier = tier
                break

        base_kes = Decimal(str(base_prices[matched_tier]))
        factor = Decimal(str(device_factors.get(self.storage_type or '', 1.0)))
        raw = base_kes * factor

        # Round to nearest 50 KES for clean pricing
        rounded = int(round(float(raw) / 50) * 50)
        return Decimal(str(max(rounded, minimum)))

    def calculate_tax(self, subtotal):
        """Return tax amount for the given subtotal. Rate defined in STORAGE_PRICING."""
        from decimal import Decimal
        rate = Decimal(self.STORAGE_PRICING['tax_rate'])
        return (subtotal * rate).quantize(Decimal('0.01'))

    def recalculate_totals(self, payment_plan=None):
        """
        Recalculate shipping, tax, total, and split amounts, then persist.
        Returns the final total (KES Decimal).
        """
        from decimal import Decimal
        base_price = self._calculate_base_price()
        shipping   = self.calculate_shipping_cost()
        tax        = self.calculate_tax(base_price + shipping)
        total      = base_price + shipping + tax

        self.payment_amount = total
        if payment_plan:
            self.payment_plan = payment_plan

        if self.payment_plan == 'half':
            half = (total / Decimal('2')).quantize(Decimal('0.01'))
            self.amount_paid = half
            self.balance_due = total - half
        else:
            self.amount_paid = total
            self.balance_due = Decimal('0.00')

        self.save(update_fields=['payment_amount', 'payment_plan', 'amount_paid', 'balance_due'])
        return total

    # =========================================================================
    # Properties
    # =========================================================================

    @property
    def total_downloaded_files(self):
        if self.download_log and 'successful_downloads' in self.download_log:
            return self.download_log['successful_downloads']
        if self.download_log and 'downloaded' in self.download_log:
            return self.download_log['downloaded']
        return self.file_selections.filter(download_status='downloaded').count()

    @property
    def total_failed_files(self):
        if self.download_log and 'failed_downloads' in self.download_log:
            return self.download_log['failed_downloads']
        if self.download_log and 'failed' in self.download_log:
            return self.download_log['failed']
        return self.file_selections.filter(download_status='failed').count()

    @property
    def total_files_count(self):
        if self.download_log and 'total_files' in self.download_log:
            return self.download_log['total_files']
        return self.total_files or self.file_selections.count()

    @property
    def total_downloaded_size_bytes(self):
        if self.download_log and 'total_downloaded_size' in self.download_log:
            return self.download_log['total_downloaded_size']
        from django.db.models import Sum
        total = self.file_selections.filter(download_status='downloaded').aggregate(
            total=Sum('download_size')
        )['total']
        return total or 0

    def total_downloaded_size_display(self):
        bytes_size = self.total_downloaded_size_bytes
        if bytes_size == 0:
            return "0 GB"
        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if bytes_size < 1024:
                return f"{bytes_size:.2f} {unit}"
            bytes_size /= 1024
        return f"{bytes_size:.2f} PB"

    @property
    def download_summary(self):
        return f"{self.total_downloaded_files}/{self.total_files_count}"

    @property
    def display_id(self):
        return self.order_number

    @property
    def is_downloadable(self):
        return self.status in [
            'payment_completed', 'partially_paid', 'storage_selected',
            'download_failed', 'download_partial',
        ]

    @property
    def requires_payment(self):
        return self.status in ['storage_selected', 'payment_pending']

    @property
    def download_ready(self):
        try:
            return self.download_task.status == 'completed' and bool(self.download_task.temp_zip_path)
        except DownloadTask.DoesNotExist:
            return False


class DownloadTask(models.Model):
    """Tracks the Celery async download task for an order."""
    STATUS_CHOICES = [
        ('queued', 'Queued'),
        ('in_progress', 'In Progress'),
        ('completed', 'Completed'),
        ('failed', 'Failed'),
        ('retrying', 'Retrying'),
    ]

    order = models.OneToOneField(
        TransferOrder, on_delete=models.CASCADE, related_name='download_task'
    )
    celery_task_id = models.CharField(max_length=255, blank=True, null=True, db_index=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued')
    progress = models.IntegerField(default=0, help_text="Overall progress 0-100")
    error_message = models.TextField(blank=True, null=True)

    temp_zip_path = models.CharField(max_length=1000, blank=True, null=True)
    total_size_downloaded = models.BigIntegerField(default=0, help_text="Bytes")

    started_at = models.DateTimeField(null=True, blank=True)
    completed_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _("Download Task")
        verbose_name_plural = _("Download Tasks")

    def __str__(self):
        return f"DownloadTask for Order {self.order.order_number} [{self.status}]"

    @property
    def zip_exists(self):
        import os
        return bool(self.temp_zip_path) and os.path.exists(self.temp_zip_path)

    @property
    def duration_seconds(self):
        if self.started_at and self.completed_at:
            return (self.completed_at - self.started_at).total_seconds()
        return None


class FileSelection(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    order = models.ForeignKey(TransferOrder, on_delete=models.CASCADE, related_name='file_selections')

    file_id = models.CharField(max_length=255, help_text="Cloud provider's file ID")
    file_name = models.CharField(max_length=500)
    downloaded_filename = models.CharField(max_length=500, blank=True, null=True)
    file_path = models.TextField(blank=True, null=True)
    file_size = models.BigIntegerField(default=0, help_text="Size in bytes")
    file_size_gb = models.DecimalField(max_digits=10, decimal_places=6, default=0.0)
    mime_type = models.CharField(max_length=100, blank=True, null=True)
    cloud_source = models.CharField(max_length=20, default='google_drive')

    download_size = models.BigIntegerField(null=True, blank=True)
    download_attempts = models.IntegerField(default=0)
    download_status = models.CharField(
        max_length=20,
        choices=[
            ('pending', 'Pending'),
            ('downloading', 'Downloading'),
            ('downloaded', 'Downloaded'),
            ('failed', 'Failed'),
            ('skipped', 'Skipped'),
        ],
        default='pending',
        db_index=True,
    )
    download_error = models.TextField(blank=True, null=True)
    progress_percentage = models.IntegerField(default=0)

    selected_at = models.DateTimeField(auto_now_add=True)
    downloaded_at = models.DateTimeField(null=True, blank=True)

    local_file = models.ForeignKey(
        LocalUploadedFile, on_delete=models.CASCADE,
        null=True, blank=True, related_name='selections'
    )

    class Meta:
        verbose_name = _("File Selection")
        verbose_name_plural = _("File Selections")
        ordering = ['-selected_at']
        unique_together = ['order', 'file_id']
        indexes = [
            models.Index(fields=['order', 'download_status']),
            models.Index(fields=['download_status']),
        ]

    def __str__(self):
        return f"{self.file_name} ({self.file_size} bytes)"

    def save(self, *args, **kwargs):
        if self.file_size > 0:
            self.file_size_gb = self.file_size / (1024 ** 3)
        super().save(*args, **kwargs)

    @property
    def formatted_size(self):
        size = self.file_size
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024:
                return f"{size:.2f} {unit}"
            size /= 1024
        return f"{size:.2f} TB"

    @property
    def formatted_file_size(self):
        return format_file_size(self.file_size)
