import base64
import secrets
import string
from urllib.parse import parse_qs, urlparse

from django.core.exceptions import ValidationError
from django.core.files.uploadhandler import MemoryFileUploadHandler, SkipFile

from teamvault.apps.secrets.enums import ContentType
from teamvault.apps.secrets.models import Secret, SecretChange

META_FIELDS = (
    'name',
    'description',
    'username',
    'url',
    'filename',
    'access_policy',
    'needs_changing_on_leave',
    'status',
)


def extract_url_and_params(data):
    data_as_url = urlparse(data)
    data_params = parse_qs(data_as_url.query)
    for key, value in data_params.items():
        data_params[key] = value[0]
    return data_as_url, data_params


def copy_meta_from_secret(secret: Secret) -> dict:
    return {
        'name': secret.name,
        'description': secret.description,
        'username': secret.username,
        'url': secret.url,
        'filename': secret.filename,
        'access_policy': int(secret.access_policy),
        'needs_changing_on_leave': secret.needs_changing_on_leave,
        'status': int(secret.status),
    }


def apply_snapshot_to_secret(secret: Secret, change: SecretChange) -> list[str]:
    """Apply metadata snapshot fields from a SecretChange onto a Secret.
    Returns the list of fields that changed.
    """
    dirty = []
    for f in META_FIELDS:
        val = getattr(change, f)
        if getattr(secret, f) != val:
            setattr(secret, f, val)
            dirty.append(f)
    return dirty


def serialize_add_edit_data(cleaned_data, secret):
    plaintext_data = {}
    if secret.content_type == ContentType.PASSWORD:
        cleaned_data_as_url, data_params = extract_url_and_params(cleaned_data['otp_key_data'])
        if cleaned_data.get('password'):
            plaintext_data['password'] = cleaned_data['password']
        for attr in ['secret', 'digits', 'algorithm']:
            if data_params.get(attr):
                if attr == 'secret':
                    plaintext_data['otp_key'] = data_params[attr]
                else:
                    plaintext_data[attr] = data_params[attr]
    elif secret.content_type == ContentType.FILE:
        file = cleaned_data.get('file')
        if not file:
            return plaintext_data

        try:
            plaintext_data['file_content'] = base64.b64encode(file.read()).decode()
        except Exception as e:
            raise ValidationError('File type not supported') from e

        if hasattr(file, 'name') and file.name:
            secret.filename = file.name
            secret.save()
    elif secret.content_type == ContentType.CC:
        plaintext_data = {
            'holder': cleaned_data['holder'],
            'number': cleaned_data['number'],
            'expiration_month': str(cleaned_data['expiration_month']),
            'expiration_year': str(cleaned_data['expiration_year']),
            'security_code': str(cleaned_data['security_code']),
            'password': cleaned_data['password'],
        }
    return plaintext_data


def generate_password(length, digits, upper, lower, special):
    characters = string.ascii_letters + string.digits + string.punctuation
    password = []
    password.extend(secrets.choice(string.digits) for _ in range(digits))
    password.extend(secrets.choice(string.ascii_lowercase) for _ in range(lower))
    password.extend(secrets.choice(string.ascii_uppercase) for _ in range(upper))
    password.extend(secrets.choice(string.punctuation) for _ in range(special))

    # Fill the rest of the lenght with random characters from all types
    password.extend(secrets.choice(characters) for _ in range(length - len(password)))

    # Randomly shuffle the characters, so they're not grouped by type
    secrets.SystemRandom().shuffle(password)

    return ''.join(password)


class CappedMemoryFileUploadHandler(MemoryFileUploadHandler):
    def receive_data_chunk(self, raw_data, start):
        if not self.activated:  # if the file size is too big, this handler will not be activated
            # if we use StopUpload here, forms will not get fully validated,
            # which leads to more form errors than we prefer
            # raise StopUpload(connection_reset=True)
            raise SkipFile()
        super().receive_data_chunk(raw_data, start)
