"""
loud_transfer.py

Handles:
  - Step 2: File-selection UI (Google Drive file browser + local upload).
  - Chunked local file upload endpoint (2 MB chunks, server-side assembly).
  - Transfer-order creation.

Chunked upload protocol
───────────────────────
POST /upload/chunk/
    Content-Type: multipart/form-data
    Fields:
        upload_id       — client-generated UUID (same for all chunks of a file)
        chunk_index     — 0-based index
        total_chunks    — total number of chunks
        filename        — original filename
        total_size      — total file size in bytes
        chunk           — the binary chunk (file part)

Response per chunk:
    { "status": "chunk_received", "chunk_index": N, "received": N+1, "total": M }

Response on final chunk (chunk_index == total_chunks - 1):
    { "status": "complete", "file_id": "<uuid>", "name": "…", "size": N }

POST /upload/local/  (kept for single small files, max ~50 MB)
    Standard single-shot upload — backwards compatible with existing frontend.
"""

import json
import logging
import os
import shutil

from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.contrib import messages
from django.conf import settings
from rest_framework import viewsets
from rest_framework.decorators import action

from .base import DummySerializer
from ..models import LocalUploadedFile, ChunkedUploadPart, TransferOrder, FileSelection

logger = logging.getLogger(__name__)

#CHUNK_UPLOAD_TEMP_DIR = getattr(settings, 'CHUNK_UPLOAD_TEMP_DIR', '/tmp/notcloudstorage/chunks')
MAX_SINGLE_UPLOAD_BYTES = 100 * 1024 * 1024   # 100 MB — above this, require chunked upload
MAX_TOTAL_UPLOAD_BYTES = 10 * 1024 * 1024 * 1024  # 10 GB per order


class CloudTransferViewSet(viewsets.ModelViewSet):
    serializer_class = DummySerializer

    # ── Step 2: file-selection page ────────────────────────────────────────

    @action(detail=False, methods=['get', 'post'], url_path=r'')
    def new_transfer(self, request):
        base_domain = request.build_absolute_uri('/')
        user = request.user

        if not user.is_authenticated:
            return redirect(f'{base_domain}login/')

        cloud_source = request.session.get('selected_cloud')
        if not cloud_source:
            messages.error(request, "Please connect a cloud account first.")
            return redirect(f'{base_domain}transfer/cloud-connect/')

        context = {
            'base_domain': base_domain,
            'user': user,
            'cloud_source': cloud_source,
            'current_step': 2,
        }

        if request.method == 'POST':
            selected_files = request.POST.getlist('cloud_files')
            uploaded_files_json = request.POST.getlist('uploaded_files')

            if not selected_files and not uploaded_files_json:
                messages.error(request, "Please select at least one file to transfer.")
                return self._render_file_selection(request, context, cloud_source, user)

            try:
                order = self._create_transfer_order(request, user, cloud_source, selected_files)
                request.session['current_order_id'] = str(order.id)
                request.session.modified = True
                return redirect(f'{base_domain}transfer/storage/')
            except Exception as exc:
                logger.error("Transfer creation failed: %s", exc, exc_info=True)
                messages.error(request, f"Error creating transfer: {exc}")
                return self._render_file_selection(request, context, cloud_source, user)

        return self._render_file_selection(request, context, cloud_source, user)

    # ── Chunked upload endpoint ────────────────────────────────────────────

    @action(detail=False, methods=['post'], url_path='upload/chunk')
    def upload_chunk(self, request):
        """
        Receives one chunk of a large file upload.
        When all chunks are received, assembles them into a LocalUploadedFile.
        """
        if not request.user.is_authenticated:
            return JsonResponse({'error': 'Login required'}, status=401)

        upload_id = request.POST.get('upload_id', '').strip()
        filename = request.POST.get('filename', '').strip()
        chunk_index = request.POST.get('chunk_index')
        total_chunks = request.POST.get('total_chunks')
        total_size = request.POST.get('total_size', 0)
        chunk_file = request.FILES.get('chunk')

        # Validate
        if not all([upload_id, filename, chunk_index is not None, total_chunks, chunk_file]):
            return JsonResponse({'error': 'Missing required fields'}, status=400)

        try:
            chunk_index = int(chunk_index)
            total_chunks = int(total_chunks)
            total_size = int(total_size)
        except (ValueError, TypeError):
            return JsonResponse({'error': 'Invalid chunk metadata'}, status=400)

        if not _is_safe_upload_id(upload_id):
            return JsonResponse({'error': 'Invalid upload_id'}, status=400)

        # Save chunk to disk
        chunk_dir = os.path.join(settings.CHUNK_UPLOAD_TEMP_DIR, upload_id)
        os.makedirs(chunk_dir, exist_ok=True)
        chunk_path = os.path.join(chunk_dir, f'{chunk_index:06d}')

        try:
            with open(chunk_path, 'wb') as f:
                for data in chunk_file.chunks(chunk_size=2 * 1024 * 1024):
                    f.write(data)
        except OSError as exc:
            logger.error("Failed to write chunk %d for %s: %s", chunk_index, upload_id, exc)
            return JsonResponse({'error': 'Failed to save chunk to disk'}, status=500)

        # Record in DB
        ChunkedUploadPart.objects.get_or_create(
            upload_id=upload_id,
            chunk_index=chunk_index,
            defaults={'temp_path': chunk_path, 'size': os.path.getsize(chunk_path)},
        )

        received_count = ChunkedUploadPart.objects.filter(upload_id=upload_id).count()

        if received_count < total_chunks:
            return JsonResponse({
                'status': 'chunk_received',
                'chunk_index': chunk_index,
                'received': received_count,
                'total': total_chunks,
            })

        # All chunks received — assemble
        try:
            uploaded_file = _assemble_chunks(
                upload_id=upload_id,
                filename=filename,
                total_size=total_size,
                total_chunks=total_chunks,
                user=request.user,
                chunk_dir=chunk_dir,
            )
            return JsonResponse({
                'status': 'complete',
                'file_id': str(uploaded_file.id),
                'name': uploaded_file.original_name,
                'size': uploaded_file.file_size,
                'uploaded_at': uploaded_file.uploaded_at.isoformat(),
            })
        except Exception as exc:
            logger.error("Assembly failed for upload %s: %s", upload_id, exc, exc_info=True)
            # Clean up
            shutil.rmtree(chunk_dir, ignore_errors=True)
            ChunkedUploadPart.objects.filter(upload_id=upload_id).delete()
            return JsonResponse({'error': f'File assembly failed: {exc}'}, status=500)

    # ── Single-shot upload (small files) ───────────────────────────────────

    @action(detail=False, methods=['post'], url_path='upload/local')
    def upload_local_file(self, request):
        """
        Single-shot upload for files under MAX_SINGLE_UPLOAD_BYTES.
        Larger files must use the chunked endpoint.
        """
        if not request.user.is_authenticated:
            return JsonResponse({'error': 'Login required'}, status=401)

        file_obj = request.FILES.get('file')
        if not file_obj:
            return JsonResponse({'error': 'No file provided'}, status=400)

        if file_obj.size > MAX_SINGLE_UPLOAD_BYTES:
            return JsonResponse({
                'error': (
                    f'File too large for single upload ({_fmt(file_obj.size)}). '
                    'Please use the chunked upload method for files over 100 MB.'
                )
            }, status=413)

        try:
            uploaded_file = LocalUploadedFile.objects.create(
                user=request.user,
                original_name=file_obj.name,
                file_size=file_obj.size,
                file=file_obj,
                total_chunks=1,
                received_chunks=1,
                upload_complete=True,
            )
            return JsonResponse({
                'success': True,
                'file_id': str(uploaded_file.id),
                'name': uploaded_file.original_name,
                'size': uploaded_file.file_size,
                'uploaded_at': uploaded_file.uploaded_at.isoformat(),
            })
        except Exception as exc:
            logger.error("Single upload error: %s", exc, exc_info=True)
            return JsonResponse({'error': str(exc)}, status=500)

   
   
    @action(detail=False, methods=['get'], url_path='browse')
    def browse_folder(self, request):
        """
        AJAX endpoint to fetch folder contents for a given folder_id.
        Query param: ?folder_id=<google_drive_folder_id>
        """
        if not request.user.is_authenticated:
            return JsonResponse({'error': 'Login required'}, status=401)

        folder_id = request.GET.get('folder_id', 'root')
        # Validate folder_id (alphanumeric, hyphens, underscores – typical Drive IDs)
        if not folder_id or not isinstance(folder_id, str):
            return JsonResponse({'error': 'Invalid folder_id'}, status=400)

        try:
            data = self._get_google_drive_data(request.user, folder_id)
            return JsonResponse(data)
        except Exception as exc:
            logger.error("browse_folder failed: %s", exc, exc_info=True)
            return JsonResponse({'error': str(exc)}, status=500)
   
   
   
    # ── Transfer order creation ────────────────────────────────────────────

    def _create_transfer_order(self, request, user, cloud_source, selected_cloud_file_ids):
        from django.db import transaction

        uploaded_files_data = []
        for raw in request.POST.getlist('uploaded_files', []):
            try:
                uploaded_files_data.append(json.loads(raw))
            except json.JSONDecodeError:
                continue

        uploaded_file_ids = [d.get('id') for d in uploaded_files_data if d.get('id')]
        local_files = LocalUploadedFile.objects.filter(
            id__in=uploaded_file_ids, user=user, upload_complete=True
        )

        with transaction.atomic():
            order = TransferOrder.objects.create(
                user=user,
                cloud_source=cloud_source,
                status='files_selected',
                total_files=len(selected_cloud_file_ids) + local_files.count(),
                total_size=0,
            )

            # Store OAuth tokens on the order for the download task to use later
            self._attach_oauth_tokens(order, user)

            local_files.update(transfer_order=order)

            selections = []
            total_bytes = 0

            # Cloud files — fetch metadata in one batch call
            if selected_cloud_file_ids:
                cloud_meta = self._batch_file_metadata(selected_cloud_file_ids, cloud_source, user)
                for fid in selected_cloud_file_ids:
                    info = cloud_meta.get(fid) or {}
                    size = info.get('sizeBytes', 0)
                    total_bytes += size
                    selections.append(FileSelection(
                        order=order,
                        file_id=fid,
                        file_name=info.get('name', 'Unknown'),
                        file_size=size,
                        mime_type=info.get('mimeType', ''),
                        cloud_source=cloud_source,
                        file_path=info.get('webViewLink') or info.get('webUrl', ''),
                    ))

            # Local files
            for lf in local_files:
                total_bytes += lf.file_size
                selections.append(FileSelection(
                    order=order,
                    file_id=str(lf.id),
                    file_name=lf.original_name,
                    file_size=lf.file_size,
                    cloud_source='local_upload',
                    file_path=lf.file.url,
                    local_file=lf,
                ))

            FileSelection.objects.bulk_create(selections)

            order.total_size = total_bytes / (1024 ** 3)
            order.save(update_fields=['total_size'])

        return order

    def _attach_oauth_tokens(self, order, user):
        """Copy the user's current OAuth tokens to the order for later background use."""
        try:
            from social_django.models import UserSocialAuth
            sa = UserSocialAuth.objects.filter(user=user, provider='google-oauth2').first()
            if sa:
                from django.utils import timezone
                order.access_token = sa.extra_data.get('access_token')
                order.refresh_token = sa.extra_data.get('refresh_token')
                order.token_expiry = timezone.now() + timezone.timedelta(hours=1)
                order.save(update_fields=['access_token', 'refresh_token', 'token_expiry'])
        except Exception as exc:
            logger.warning("Failed to attach OAuth tokens to order: %s", exc)

    def _batch_file_metadata(self, file_ids, cloud_source, user):
        """Return {file_id: metadata_dict} for all selected IDs."""
        cloud_data = self._get_cloud_data(cloud_source, user)
        files = cloud_data.get('files', [])
        lookup = {f['id']: f for f in files}
        return {fid: lookup.get(fid) for fid in file_ids}

    # ── Render helpers ─────────────────────────────────────────────────────

    def _render_file_selection(self, request, context, cloud_source, user):
        cloud_data = self._get_cloud_data(cloud_source, user)
        context.update(cloud_data)
        return render(request, 'transfer.html', context)

    def _get_cloud_data(self, cloud_source, user):
        if cloud_source == 'google_drive':
            return self._get_google_drive_data(user)
        elif cloud_source == 'dropbox':
            return self._get_dropbox_stub()
        elif cloud_source == 'onedrive':
            return self._get_onedrive_stub()
        return {}
    
    def _get_google_drive_data(self, user):
        try:
            from social_django.models import UserSocialAuth
            from google.oauth2.credentials import Credentials
            from googleapiclient.discovery import build
            from googleapiclient.errors import HttpError
            from datetime import datetime, timezone as dt_tz

            sa = UserSocialAuth.objects.filter(user=user, provider='google-oauth2').first()
            if not sa or not sa.extra_data.get('access_token'):
                return self._drive_error_context('Please reconnect your Google Drive account.')

            creds = Credentials(token=sa.extra_data['access_token'])
            service = build('drive', 'v3', credentials=creds, cache_discovery=False)

            about = service.about().get(fields='storageQuota,user').execute()
            sq = about.get('storageQuota', {})
            ui = about.get('user', {})

            used = int(sq.get('usage', 0))
            total = int(sq.get('limit', 0))
            pct = round(used / total * 100, 1) if total else 0
            used_gb = used / 1024 ** 3
            total_gb = total / 1024 ** 3

            # Paginate up to 1000 files, most recently modified first
            files = []
            page_token = None
            while len(files) < 1000:
                resp = service.files().list(
                    pageSize=200,
                    fields='nextPageToken,files(id,name,mimeType,size,modifiedTime,webViewLink)',
                    orderBy='modifiedTime desc',
                    pageToken=page_token,
                ).execute()
                batch = resp.get('files', [])
                files.extend(batch)
                page_token = resp.get('nextPageToken')
                if not page_token:
                    break

            formatted = []
            now = datetime.now(dt_tz.utc)
            for f in files:
                if f.get('mimeType') == 'application/vnd.google-apps.shortcut':
                    continue
                size_b = int(f.get('size', 0))
                formatted.append({
                    'id': f['id'],
                    'name': f['name'],
                    'size': _fmt(size_b),
                    'sizeBytes': size_b,
                    'mimeType': f.get('mimeType', ''),
                    'modified_display': _relative_time(f.get('modifiedTime'), now),
                    'folder': f.get('mimeType') == 'application/vnd.google-apps.folder',
                    'webViewLink': f.get('webViewLink', ''),
                })

            categories = self._categorise_files(formatted)

            return {
                'cloud_connected': True,
                'cloud_service': 'Google Drive',
                'user_email': ui.get('emailAddress', ''),
                'storage_info': {
                    'used': f'{used_gb:.1f} GB',
                    'total': f'{total_gb:.0f} GB',
                    'percentage': pct,
                    'available': f'{total_gb - used_gb:.1f} GB',
                    'quota_type': 'free' if total_gb <= 15 else 'premium',
                },
                'files': formatted,
                'categories': categories,
                'total_files': len(formatted),
                'total_size': f'{used_gb:.1f} GB',
            }

        except Exception as exc:
            logger.error("Google Drive data error: %s", exc, exc_info=True)
            return self._drive_error_context(str(exc))


    @staticmethod
    def _drive_error_context(msg):
        return {
            'cloud_connected': False,
            'cloud_service': 'Google Drive',
            'user_email': '',
            'storage_info': {'used': '0 GB', 'total': '0 GB', 'percentage': 0,
                             'available': '0 GB', 'quota_type': 'free'},
            'files': [],
            'categories': [],
            'total_files': 0,
            'total_size': '0 GB',
            'error': msg,
        }

    @staticmethod
    def _categorise_files(files):
        buckets = {
            'documents': {'name': 'Documents', 'count': 0, 'icon': 'file-text', 'color': 'blue'},
            'images':    {'name': 'Images',    'count': 0, 'icon': 'image',     'color': 'purple'},
            'videos':    {'name': 'Videos',    'count': 0, 'icon': 'video',     'color': 'red'},
            'pdfs':      {'name': 'PDFs',      'count': 0, 'icon': 'file-pdf',  'color': 'red'},
            'sheets':    {'name': 'Sheets',    'count': 0, 'icon': 'file-excel','color': 'green'},
            'slides':    {'name': 'Slides',    'count': 0, 'icon': 'file-powerpoint', 'color': 'orange'},
            'audio':     {'name': 'Audio',     'count': 0, 'icon': 'music',     'color': 'indigo'},
            'others':    {'name': 'Others',    'count': 0, 'icon': 'file',      'color': 'gray'},
        }
        for f in files:
            m = f.get('mimeType', '')
            if 'google-apps.document' in m or 'word' in m:
                buckets['documents']['count'] += 1
            elif 'google-apps.spreadsheet' in m or 'sheet' in m:
                buckets['sheets']['count'] += 1
            elif 'google-apps.presentation' in m or 'presentation' in m:
                buckets['slides']['count'] += 1
            elif 'pdf' in m:
                buckets['pdfs']['count'] += 1
            elif m.startswith('image/'):
                buckets['images']['count'] += 1
            elif m.startswith('video/'):
                buckets['videos']['count'] += 1
            elif m.startswith('audio/'):
                buckets['audio']['count'] += 1
            elif 'folder' not in m and 'shortcut' not in m:
                buckets['others']['count'] += 1

        return sorted(
            [b for b in buckets.values() if b['count'] > 0],
            key=lambda x: x['count'], reverse=True,
        )

    @staticmethod
    def _get_dropbox_stub():
        return {
            'cloud_connected': False,
            'cloud_service': 'Dropbox',
            'files': [],
            'categories': [],
            'error': 'Dropbox integration coming soon.',
        }

    @staticmethod
    def _get_onedrive_stub():
        return {
            'cloud_connected': False,
            'cloud_service': 'OneDrive',
            'files': [],
            'categories': [],
            'error': 'OneDrive integration coming soon.',
        }


# ---------------------------------------------------------------------------
# Chunk assembly helper (module-level for clarity)
# ---------------------------------------------------------------------------

def _assemble_chunks(upload_id, filename, total_size, total_chunks, user, chunk_dir):
    """
    Read chunks in order, write to the final media path,
    create a LocalUploadedFile record, clean up temp files.
    """
    import uuid
    from datetime import date

    # Determine final path (mirrors Django's upload_to)
    today = date.today()
    rel_dir = f"local_uploads/{today.year}/{today.month:02d}/{today.day:02d}"
    abs_dir = os.path.join(settings.MEDIA_ROOT, rel_dir)
    os.makedirs(abs_dir, exist_ok=True)

    # Ensure unique filename
    safe_name = os.path.basename(filename)
    dest_path = os.path.join(abs_dir, safe_name)
    if os.path.exists(dest_path):
        base, ext = os.path.splitext(safe_name)
        safe_name = f"{base}_{uuid.uuid4().hex[:8]}{ext}"
        dest_path = os.path.join(abs_dir, safe_name)

    written = 0
    try:
        with open(dest_path, 'wb') as out:
            for i in range(total_chunks):
                chunk_path = os.path.join(chunk_dir, f'{i:06d}')
                if not os.path.exists(chunk_path):
                    raise FileNotFoundError(f"Missing chunk {i} for upload {upload_id}")
                with open(chunk_path, 'rb') as c:
                    while data := c.read(4 * 1024 * 1024):
                        out.write(data)
                        written += len(data)
    except Exception:
        # Remove partial file on failure
        if os.path.exists(dest_path):
            os.remove(dest_path)
        raise
    
    

    # Create DB record with the assembled file
    rel_path = os.path.join(rel_dir, safe_name)
    uploaded_file = LocalUploadedFile.objects.create(
        user=user,
        original_name=filename,
        file_size=written,
        total_chunks=total_chunks,
        received_chunks=total_chunks,
        upload_complete=True,
    )
    # Set file field without re-uploading
    uploaded_file.file.name = rel_path
    uploaded_file.save(update_fields=['file'])

    # Cleanup
    shutil.rmtree(chunk_dir, ignore_errors=True)
    ChunkedUploadPart.objects.filter(upload_id=upload_id).delete()

    logger.info("Assembled upload %s -> %s (%.1f MB)", upload_id, rel_path, written / 1024 / 1024)
    return uploaded_file


# ---------------------------------------------------------------------------
# Utility
# ---------------------------------------------------------------------------

def _is_safe_upload_id(uid: str) -> bool:
    """Only allow hex / UUID-like IDs to prevent path traversal."""
    import re
    return bool(re.match(r'^[a-zA-Z0-9\-_]{8,64}$', uid))


def _fmt(size_bytes: int) -> str:
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if size_bytes < 1024:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024
    return f"{size_bytes:.1f} PB"


def _relative_time(iso_str, now):
    if not iso_str:
        return 'Unknown'
    try:
        from datetime import datetime, timezone as dt_tz
        dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
        diff = now - dt
        days = diff.days
        secs = diff.seconds
        if days == 0:
            if secs < 60:
                return 'Just now'
            if secs < 3600:
                return f"{secs // 60}m ago"
            return f"{secs // 3600}h ago"
        if days == 1:
            return 'Yesterday'
        if days < 7:
            return f"{days} days ago"
        if days < 30:
            return f"{days // 7}w ago"
        return dt.strftime('%b %d, %Y')
    except Exception:
        return 'Unknown'
