import os
import shutil

from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import status

from common.api.generic import JMSBulkModelViewSet
from common.exceptions import JMSException
from common.permissions import IsOwnerOrAdminWritable
from common.utils.http import is_true
from common.utils.zip import safe_extract_zip
from rbac.permissions import RBACPermission
from ..const import Scope
from ..exception import PlaybookNoValidEntry
from ..models import Playbook
from ..serializers.playbook import PlaybookSerializer

__all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"]

from rest_framework.views import APIView
from rest_framework.response import Response
from django.utils._os import safe_join


def unzip_playbook(src, dest):
    safe_extract_zip(src, dest)


class PlaybookViewSet(JMSBulkModelViewSet):
    serializer_class = PlaybookSerializer
    permission_classes = (RBACPermission, IsOwnerOrAdminWritable)
    queryset = Playbook.objects.all()
    search_fields = ('name', 'comment')
    filterset_fields = ['scope', 'creator']

    def allow_bulk_destroy(self, qs, filtered):
        for obj in filtered:
            self.check_object_permissions(self.request, obj)
        return True

    def perform_destroy(self, instance):
        if instance.job_set.exists():
            raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")})
        instance_id = instance.id
        super().perform_destroy(instance)
        dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__())
        if os.path.exists(dest_path):
            shutil.rmtree(dest_path)

    def get_queryset(self):
        queryset = super().get_queryset()
        user = self.request.user
        if is_true(self.request.query_params.get('only_mine')):
            queryset = queryset.filter(creator=user)
        else:
            queryset = queryset.filter(Q(creator=user) | Q(scope=Scope.public))
        return queryset

    def perform_create(self, serializer):
        instance = serializer.save()
        base_path = safe_join(settings.DATA_DIR, "ops", "playbook")
        clone_id = self.request.query_params.get('clone_from')

        if clone_id:
            queryset = self.get_queryset()
            clone_from = queryset.filter(id=clone_id).first()
            if not clone_from:
                raise JMSException(code='invalid_clone_id', detail={"msg": "Invalid clone_from id"})
            src_path = safe_join(base_path, clone_id)
            dest_path = safe_join(base_path, str(instance.id))
            if not os.path.exists(src_path):
                raise JMSException(code='invalid_playbook_id', detail={"msg": "clone playbook file not found"})
            shutil.copytree(src_path, dest_path)
            return

        if 'multipart/form-data' in self.request.headers['Content-Type']:
            src_path = safe_join(settings.MEDIA_ROOT, instance.path.name)
            dest_path = safe_join(base_path, str(instance.id))

            try:
                unzip_playbook(src_path, dest_path)
            except RuntimeError:
                raise JMSException(code='invalid_playbook_file', detail={"msg": "Unzip failed"})

            if 'main.yml' not in os.listdir(dest_path):
                raise PlaybookNoValidEntry

        elif instance.create_method == 'blank':
            dest_path = safe_join(base_path, str(instance.id))
            os.makedirs(dest_path)
            with open(safe_join(dest_path, 'main.yml'), 'w') as f:
                f.write('## write your playbook here')


class PlaybookFileBrowserAPIView(APIView):
    permission_classes = (RBACPermission,)
    rbac_perms = {
        'GET': 'ops.view_playbook',
        'POST': 'ops.change_playbook',
        'DELETE': 'ops.change_playbook',
        'PATCH': 'ops.change_playbook',
    }
    protected_files = ['root', 'main.yml']

    def get_playbook(self, playbook_id):
        playbook = get_object_or_404(Playbook, id=playbook_id, creator=self.request.user)
        return playbook

    def get(self, request, **kwargs):
        playbook_id = kwargs.get('pk')
        user = self.request.user
        playbook = get_object_or_404(Playbook, Q(creator=user) | Q(scope=Scope.public), id=playbook_id)
        work_path = playbook.work_dir
        file_key = request.query_params.get('key', '')
        if file_key:
            try:
                file_path = safe_join(work_path, file_key)
                with open(file_path, 'r') as f:
                    content = f.read()
            except UnicodeDecodeError:
                content = _('Unsupported file content')
            except (SuspiciousFileOperation, FileNotFoundError):
                raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
            return Response({'content': content})
        else:
            expand_key = request.query_params.get('expand', '')
            nodes = self.generate_tree(playbook, work_path, expand_key)
            return Response(nodes)

    def post(self, request, **kwargs):
        playbook_id = kwargs.get('pk')
        playbook = self.get_playbook(playbook_id)
        work_path = playbook.work_dir

        parent_key = request.data.get('key', '')
        if parent_key == 'root':
            parent_key = ''
        if os.path.dirname(parent_key) == 'root':
            parent_key = os.path.basename(parent_key)

        full_path = safe_join(work_path, parent_key)

        is_directory = request.data.get('is_directory', False)
        content = request.data.get('content', '')
        name = request.data.get('name', '')

        def find_new_name(p, is_file=False):
            if not p:
                if is_file:
                    p = 'new_file.yml'
                else:
                    p = 'new_dir'
            np = safe_join(full_path, p)
            n = 0
            while os.path.exists(np):
                n += 1
                np = safe_join(full_path, '{}({})'.format(p, n))
            return np

        try:
            if is_directory:
                new_file_path = find_new_name(name)
                os.makedirs(new_file_path)
            else:
                new_file_path = find_new_name(name, True)
                with open(new_file_path, 'w') as f:
                    f.write(content)
        except SuspiciousFileOperation:
            raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})

        relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path)
        new_node = {
            "name": os.path.basename(new_file_path),
            "title": os.path.basename(new_file_path),
            "id": safe_join(relative_path, os.path.basename(new_file_path))
            if not safe_join(relative_path, os.path.basename(new_file_path)).startswith('.')
            else os.path.basename(new_file_path),
            "isParent": is_directory,
            "pId": relative_path if not relative_path.startswith('.') else 'root',
            "open": True,
        }
        if not is_directory:
            new_node['iconSkin'] = 'file'
        return Response(new_node)

    def patch(self, request, **kwargs):
        playbook_id = kwargs.get('pk')
        playbook = self.get_playbook(playbook_id)
        work_path = playbook.work_dir

        file_key = request.data.get('key', '')
        new_name = request.data.get('new_name', '')

        if file_key in self.protected_files and new_name:
            raise JMSException(code='this_file_can_not_be_rename', detail={"msg": _("This file can not be rename")})

        if os.path.dirname(file_key) == 'root':
            file_key = os.path.basename(file_key)

        content = request.data.get('content', '')
        is_directory = request.data.get('is_directory', False)

        if not file_key or file_key == 'root':
            return Response(status=status.HTTP_400_BAD_REQUEST)
        file_path = safe_join(work_path, file_key)

        # rename
        if new_name:
            try:
                new_file_path = safe_join(os.path.dirname(file_path), new_name)
                if new_file_path == file_path:
                    return Response(status=status.HTTP_200_OK)
                if os.path.exists(new_file_path):
                    raise JMSException(code='file_already exists', detail={"msg": _("File already exists")})
                os.rename(file_path, new_file_path)
            except SuspiciousFileOperation:
                raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})

        # edit content
        else:
            if not is_directory:
                with open(file_path, 'w') as f:
                    f.write(content)
        return Response(status=status.HTTP_200_OK)

    def delete(self, request, **kwargs):
        playbook_id = kwargs.get('pk')
        playbook = self.get_playbook(playbook_id)
        work_path = playbook.work_dir
        file_key = request.query_params.get('key', '')
        if not file_key:
            raise JMSException(code='file_key_is_required', detail={"msg": _("File key is required")})

        if file_key in self.protected_files:
            raise JMSException(code='This file can not be delete', detail={"msg": _("This file can not be delete")})
        file_path = safe_join(work_path, file_key)
        if os.path.isdir(file_path):
            shutil.rmtree(file_path)
        else:
            os.remove(file_path)
        return Response({'msg': 'ok'})

    @staticmethod
    def generate_tree(playbook, root_path, expand_key=None):
        nodes = [{
            "name": playbook.name,
            "title": playbook.name,
            "id": 'root',
            "isParent": True,
            "open": True,
            "pId": '',
            "temp": False
        }]
        for path, dirs, files in os.walk(root_path):
            dirs.sort()
            files.sort()

            relative_path = os.path.relpath(path, root_path)
            for d in dirs:
                dir_id = os.path.relpath(safe_join(path, d), root_path)

                node = {
                    "name": d,
                    "title": d,
                    "id": dir_id,
                    "isParent": True,
                    "open": True,
                    "pId": relative_path if not relative_path.startswith('.') else 'root',
                    "temp": False
                }
                if expand_key == node['id']:
                    node['open'] = True
                nodes.append(node)
            for f in files:
                file_id = os.path.relpath(safe_join(path, f), root_path)
                node = {
                    "name": f,
                    "title": f,
                    "iconSkin": 'file',
                    "id": file_id,
                    "isParent": False,
                    "open": False,
                    "pId": relative_path if not relative_path.startswith('.') else 'root',
                    "temp": False
                }
                nodes.append(node)
        return nodes
