from django.http import HttpResponse
from django.urls import path
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from authentication.signature_auth import SignatureAuthentication
from toolshed.offlinedata import (
    inventory_rows, friend_rows, location_rows, inventory_files, rows_to_csv,
    import_locations, import_friends, import_inventory,
    profile_data, profile_picture_files, settings_data, import_profile, import_settings,
    delete_user_data, delete_user_account,
)


def local_user_or_none(identity):
    """Resolve the local ToolshedUser associated with an authenticated KnownIdentity, if any.

    Returns None if the identity belongs to an external/remote user with no local account.
    """
    if identity is None:
        return None
    return identity.user.first()


def user_data(user):
    import io
    import json
    import zipfile

    zip_buffer = io.BytesIO()

    with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
        zip_file.writestr('profile.json', json.dumps(profile_data(user)))
        zip_file.writestr('settings.json', json.dumps(settings_data(user)))

        inventory_csv = b''.join(rows_to_csv(inventory_rows(user)))
        zip_file.writestr('inventory.csv', inventory_csv)

        friends_csv = b''.join(rows_to_csv(friend_rows(user)))
        zip_file.writestr('friends.csv', friends_csv)

        locations_csv = b''.join(rows_to_csv(location_rows(user)))
        zip_file.writestr('locations.csv', locations_csv)

        written_files = set()
        for arcname, data in profile_picture_files(user):
            zip_file.writestr(arcname, data)
            written_files.add(arcname)

        for arcname, data in inventory_files(user):
            if arcname in written_files:
                continue
            zip_file.writestr(arcname, data)
            written_files.add(arcname)

    return zip_buffer.getvalue()


def parse_user_data(data):
    import io
    import zipfile

    with zipfile.ZipFile(io.BytesIO(data), "r") as zip_file:
        for file_name in zip_file.namelist():
            yield file_name, zip_file.read(file_name)


def import_files(zip_file):
    """Fault-tolerant extraction of the 'files/' subfolder into File objects.

    Returns a dict mapping the zip arcname to the created/existing File instance. Entries that
    fail to read or save are silently skipped so a single corrupt attachment doesn't abort the import.
    """
    from hashlib import sha256
    import mimetypes

    from django.core.files.base import ContentFile
    from django.db import transaction
    from files.models import File

    result = {}
    for name in zip_file.namelist():
        if not name.startswith('files/') or name == 'files/':
            continue
        try:
            with transaction.atomic():
                data = zip_file.read(name)
                content_hash = sha256(data).hexdigest()
                mime_type, _ = mimetypes.guess_type(name)
                file_obj = File.objects.filter(hash=content_hash).first()
                if file_obj is None:
                    file_obj = File.objects.create(
                        file=ContentFile(data, content_hash),
                        mime_type=mime_type or 'application/octet-stream',
                        hash=content_hash,
                    )
                result[name] = file_obj
        except Exception as error:
            print(f'Skipping file "{name}" during import: {error}')
    return result



def import_user_data(user, data):
    """Fault-tolerant import of an export zip produced by `user_data()`.

    Any of 'profile.json', 'settings.json', 'inventory.csv', 'friends.csv', 'locations.csv' or
    the 'files/' subfolder may be missing; whatever is present is imported and everything else
    is silently skipped.

    Each section is imported inside its own transaction savepoint (`transaction.atomic()`), so a
    DB-level failure in one section (e.g. a profile.json whose email collides with another
    account) can't leave the connection in a broken/aborted-transaction state that would
    otherwise silently take down every subsequent section (including the inventory items and
    their properties) with an opaque "current transaction is aborted" error.
    """
    import io
    import zipfile

    from django.db import transaction

    summary = {'profile': False, 'settings': 0, 'locations': 0, 'friends': 0, 'inventory_items': 0, 'files': 0,
               'errors': []}

    with zipfile.ZipFile(io.BytesIO(data), 'r') as zip_file:
        names = set(zip_file.namelist())

        available_files = import_files(zip_file)
        summary['files'] = len(available_files)

        if 'profile.json' in names:
            try:
                with transaction.atomic():
                    summary['profile'] = import_profile(user, zip_file.read('profile.json'), available_files)
            except Exception as error:
                summary['errors'].append(f'Could not import profile.json: {error}')

        if 'settings.json' in names:
            try:
                with transaction.atomic():
                    summary['settings'] = import_settings(user, zip_file.read('settings.json'))
            except Exception as error:
                summary['errors'].append(f'Could not import settings.json: {error}')

        if 'locations.csv' in names:
            try:
                with transaction.atomic():
                    summary['locations'] = import_locations(user, zip_file.read('locations.csv'))
            except Exception as error:
                summary['errors'].append(f'Could not import locations.csv: {error}')

        if 'friends.csv' in names:
            try:
                with transaction.atomic():
                    summary['friends'] = import_friends(user, zip_file.read('friends.csv'))
            except Exception as error:
                summary['errors'].append(f'Could not import friends.csv: {error}')

        if 'inventory.csv' in names:
            try:
                with transaction.atomic():
                    summary['inventory_items'], inventory_errors = import_inventory(
                        user, zip_file.read('inventory.csv'), available_files)
                summary['errors'].extend(inventory_errors)
            except Exception as error:
                summary['errors'].append(f'Could not import inventory.csv: {error}')

    return summary



def _extract_zip_bytes(zip_payload):
    """Normalize the incoming 'zip' request payload (uploaded file, base64 string, or raw bytes)."""
    import base64

    if hasattr(zip_payload, 'read'):
        return zip_payload.read()
    if isinstance(zip_payload, str):
        try:
            return base64.b64decode(zip_payload, validate=True)
        except Exception:
            return zip_payload.encode('utf-8')
    return zip_payload


@api_view(['POST'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def import_data(request, format=None):
    local_user = local_user_or_none(request.user)
    if local_user is None:
        return Response({'detail': 'This endpoint is only available to local users'}, status=403)
    zip_payload = request.data.get('zip')
    if not zip_payload:
        return Response(status=400)
    try:
        zip_bytes = _extract_zip_bytes(zip_payload)
        summary = import_user_data(local_user, zip_bytes)
    except Exception as error:
        return Response({'detail': f'Could not read zip file: {error}'}, status=400)
    return Response(summary, status=200)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def export_data(request, format=None):
    local_user = local_user_or_none(request.user)
    if local_user is None:
        return Response({'detail': 'This endpoint is only available to local users'}, status=403)
    return HttpResponse(user_data(local_user), content_type='application/zip', status=200)


@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def delete_data(request, format=None):
    """Wipe all of the local user's data (everything included in the export), but keep the account.

    This is *not* account deletion/closure - the user stays logged in and can keep using the
    account (with all their data reset to a blank slate) afterwards.
    """
    local_user = local_user_or_none(request.user)
    if local_user is None:
        return Response({'detail': 'This endpoint is only available to local users'}, status=403)
    summary = delete_user_data(local_user)
    return Response(summary, status=200)


@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def delete_account(request, format=None):
    """Permanently close the local user's account, after wiping all of its data.

    Unlike `delete_data()`, this also removes the account itself - the user can no longer log
    in afterwards. Their public identity is kept so remote friends/history referencing it stay
    intact, but the local ToolshedUser row is gone.
    """
    local_user = local_user_or_none(request.user)
    if local_user is None:
        return Response({'detail': 'This endpoint is only available to local users'}, status=403)
    summary = delete_user_account(local_user)
    return Response(summary, status=200)


urlpatterns = [
    path('export/', export_data, name='export_data'),
    path('import/', import_data, name='import_data'),
    path('account_data/', delete_data, name='delete_data'),
    path('account/', delete_account, name='delete_account'),
]
