import datetime
import json
import re

from django import template
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView

from .forms import IssueInfoForm
from .models import (
    ISSUE_TYPES,
    ORIGIN_TYPES,
    PAID_BY_TYPES,
    InstalledService,
    InstalledVersion,
    IssueInfo,
    Module,
    Project,
)
from .utils import CommitAndIssues, Issue, decorate_commit_line, get_issue_deployment_status


def is_ajax(request):
    return request.headers.get('x-requested-with') == 'XMLHttpRequest'


class ProjectDetailView(DetailView):
    model = Project

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        platforms = self.object.platform_set.all()
        services = self.object.service_set.all()

        context['platforms'] = platforms
        context['services'] = services

        return context


class ModuleDiffView(TemplateView):
    template_name = 'projects/diff.html'

    def get_context_data(self, name, commit1, commit2, **kwargs):
        context = super().get_context_data(**kwargs)
        context['module'] = Module.objects.get(name=name)
        context['commit1'] = commit1
        context['commit2'] = commit2

        context['difflog'] = '\n'.join(
            [
                decorate_commit_line(line)
                for line in context['module'].get_diff_log(commit1, commit2).splitlines()
            ]
        )

        return context


class ModuleIssuesView(TemplateView):
    template_name = 'projects/issues.html'

    def get_context_data(self, name, commit1, commit2, **kwargs):
        context = super().get_context_data(**kwargs)
        context['module'] = Module.objects.get(name=name)
        context['commit1'] = commit1
        context['commit2'] = commit2
        commits = CommitAndIssues.get_for_commits(context['module'], commit1, commit2)
        issues = {}
        for commit in commits:
            for issue in commit.issues:
                if int(issue.id) not in issues:
                    issues[int(issue.id)] = issue
                else:
                    issue = issues[int(issue.id)]
                issue.add_commit(commit)

        issues_list = list(issues.items())
        issues_list.sort()
        context['issues'] = [x[1] for x in issues_list]

        return context


class ProjectHistoryView(DetailView):
    model = Project
    template_name_suffix = '_history'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        installed_versions = InstalledVersion.objects.filter(service__platform__project=self.object).order_by(
            '-timestamp'
        )[:1000]

        context['installed_versions'] = installed_versions

        return context


class ProjectSummaryHistoryMixin:
    # XXX: add an 'interesting' attribute to module model?
    interesting_modules = [
        'publik-base-theme',
        'publik-django-templatetags',
        'combo',
        'lingo',
        'wcs',
        'hobo',
        'authentic2',
        'welco',
        'chrono',
        'corbo',
        'passerelle',
        'fargo',
        'mandayejs',
        'bijoe',
        'wcs-olap',
        'gadjo',
        'godo.js',
    ]

    def get_history(self):
        platforms = list(self.object.platform_set.all())
        platform = platforms[-1]
        installed_versions = list(
            InstalledVersion.objects.filter(
                timestamp__gt=timezone.now() - datetime.timedelta(days=120),
                version__module__name__in=self.interesting_modules,
                service__platform=platform,
            )
            .prefetch_related('version__module')
            .exclude(version__version='')
            .order_by('-timestamp')[:1000]
        )
        installed_versions.reverse()
        versions_and_day = {}
        for installed in installed_versions:
            installed_day = installed.timestamp.strftime('%Y-%m-%d')
            if installed_day not in versions_and_day:
                versions_and_day[installed_day] = {'modules': {}, 'day': installed.timestamp.date()}
            if installed.version.module.name in versions_and_day[installed_day]['modules']:
                continue
            versions_and_day[installed_day]['modules'][installed.version.module.name] = {
                'name': installed.version.module.name,
                'current_version': installed.version.version,
            }
        for installed_day in versions_and_day.values():
            installed_day['modules'] = dict(sorted(installed_day['modules'].items()))

        versions_sorted_by_day = list(versions_and_day.values())
        versions_sorted_by_day.sort(key=lambda x: x.get('day'))

        previous_versions = {}
        for version_and_day in versions_sorted_by_day:
            for module_name, module_versions in list(version_and_day['modules'].items()):
                if module_name in previous_versions:
                    module_versions['previous_version'] = previous_versions[module_name]
                    if module_versions['previous_version'] == module_versions['current_version']:
                        del version_and_day['modules'][module_name]
                else:
                    del version_and_day['modules'][module_name]
                previous_versions[module_name] = module_versions['current_version']

        if len(platforms) > 1:
            validation_platform = platforms[-2]
            future_versions = {'modules': {}, 'day': 'future'}
            module_names = previous_versions.keys()
            for module_name in module_names:
                installed_version = (
                    InstalledVersion.objects.filter(
                        version__module__name=module_name, service__platform=validation_platform
                    )
                    .exclude(version__version='')
                    .select_related('version')
                    .order_by('-timestamp')
                    .first()
                )
                if not installed_version:
                    continue
                if not installed_version.version.version:
                    continue
                if installed_version.version.version == previous_versions[module_name]:
                    continue
                future_versions['modules'][module_name] = {
                    'name': module_name,
                    'previous_version': previous_versions[module_name],
                    'current_version': installed_version.version.version,
                }

            if future_versions['modules']:
                versions_sorted_by_day.append(future_versions)

        return versions_sorted_by_day, platform

    def get_future_version(self):
        versions_sorted_by_day, _ = self.get_history()
        last_version = versions_sorted_by_day[-1]
        if last_version['day'] == 'future':
            return last_version


class IssuesMixin:
    def get_issues(self, modules):
        modules_by_name = {m.name: m for m in Module.objects.all()}

        issues = {}
        for module_info in modules.values():
            module = modules_by_name[module_info['name']]
            if not module.repository_url:
                continue
            commits = CommitAndIssues.get_for_commits(
                module, str(module_info.get('previous_version')), str(module_info.get('current_version'))
            )
            for commit in commits:
                for issue in commit.issues:
                    if issue.subject == '---':  # private issue
                        continue
                    if int(issue.id) not in issues:
                        issues[int(issue.id)] = issue
                    else:
                        issue = issues[int(issue.id)]
                    if not hasattr(issues[int(issue.id)], 'modules'):
                        issues[int(issue.id)].modules = {}
                    issues[int(issue.id)].modules[module_info['name']] = True

        issues_info = IssueInfo.objects.filter(issue_id__in=issues.keys())
        for info in issues_info:
            issues[int(info.issue_id)].info = info

        issues = list(issues.values())
        issue_types = [x[0] for x in ISSUE_TYPES]
        issues.sort(
            key=lambda x: (
                issue_types.index(x.info.issue_type) if hasattr(x, 'info') and x.info.issue_type else 42,
                sorted(x.modules.keys()),
                int(x.id),
            )
        )
        return issues

    def get_pending_issues(self, modules, user_id=None):
        issues = self.get_issues(modules)
        if user_id:
            issues = [
                issue for issue in issues if issue.get_attribute('assigned_to', {}).get('id') == user_id
            ]

        completed_issue_ids = IssueInfo.objects.filter(
            issue_id__in=[x.id for x in issues], issue_type__isnull=False
        ).values_list('issue_id', flat=True)
        issues = [issue for issue in issues if int(issue.id) not in completed_issue_ids]

        return issues


class ProjectSummaryHistoryView(ProjectSummaryHistoryMixin, DetailView):
    model = Project
    template_name_suffix = '_summary_history'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        versions_sorted_by_day, platform = self.get_history()

        context['platform'] = platform
        context['history'] = reversed(versions_sorted_by_day)

        return context


class ProjectSummaryHistoryDayView(ProjectSummaryHistoryMixin, IssuesMixin, DetailView):
    model = Project

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        day = 'future'
        if 'year' in kwargs:
            day = datetime.date(int(kwargs['year']), int(kwargs['month']), int(kwargs['day']))

        versions_sorted_by_day, _ = self.get_history()
        version = None
        for _version in versions_sorted_by_day:
            if _version['day'] == day:
                version = _version
                break
        if not version:
            raise Http404

        issues = self.get_issues(version['modules'])
        return render(
            request,
            'projects/date_export.html',
            {'issues': issues},
        )


class IssuesMetadataStatsView(ProjectSummaryHistoryMixin, IssuesMixin, DetailView):
    model = Project

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        day = 'future'
        if 'year' in kwargs:
            day = datetime.date(int(kwargs['year']), int(kwargs['month']), int(kwargs['day']))

        versions_sorted_by_day, _ = self.get_history()
        version = None
        for _version in versions_sorted_by_day:
            if _version['day'] == day:
                version = _version
                break
        if not version:
            raise Http404

        issues = self.get_issues(version['modules'])
        issues_infos = IssueInfo.objects.filter(issue_id__in=[int(x.id) for x in issues])

        total = len(issues)
        classified = len([x for x in issues_infos if x.issue_type])
        noted_issues = len([x for x in issues_infos if x.issue_type and x.issue_type != 'TECH'])
        if not noted_issues:  # probably a hotfix release, bye
            raise Http404

        fully_classified = len([x for x in issues_infos if x.issue_type and x.paid_by and x.origin])
        fully_classified_noted = len(
            [x for x in issues_infos if x.issue_type and x.issue_type != 'TECH' and x.paid_by and x.origin]
        )

        paid_by_infos = []
        for paid_by, paid_by_str in PAID_BY_TYPES:
            count = len([x for x in issues_infos if x.paid_by == paid_by])
            count_noted = len(
                [x for x in issues_infos if x.paid_by == paid_by and x.issue_type and x.issue_type != 'TECH']
            )
            paid_by_infos.append(
                {
                    'label': paid_by_str,
                    'count': count,
                    'percent': 100 * count / total,
                    'noted': count_noted,
                    'noted_percent': 100 * count_noted / noted_issues,
                }
            )
        unknown = total - len([x for x in issues_infos if x.paid_by])
        paid_by_infos.append({'label': 'Inconnu', 'count': unknown, 'percent': 100 * unknown / total})

        origin_infos = []
        for origin, origin_str in ORIGIN_TYPES:
            count = len([x for x in issues_infos if x.origin == origin])
            count_noted = len(
                [x for x in issues_infos if x.origin == origin and x.issue_type and x.issue_type != 'TECH']
            )
            origin_infos.append(
                {
                    'label': origin_str,
                    'count': count,
                    'percent': 100 * count / total,
                    'noted': count_noted,
                    'noted_percent': 100 * count_noted / noted_issues,
                }
            )
        unknown = total - len([x for x in issues_infos if x.origin])
        origin_infos.append({'label': 'Inconnu', 'count': unknown, 'percent': 100 * unknown / total})

        return render(
            request,
            'projects/metadata_stats.html',
            {
                'version': version.get('day'),
                'total': total,
                'classified': classified,
                'classified_percent': 100 * classified / total,
                'noted_issues': noted_issues,
                'noted_issues_percent': 100 * noted_issues / total,
                'fully_classified': fully_classified,
                'fully_classified_percent': 100 * fully_classified / total,
                'fully_classified_noted': fully_classified_noted,
                'fully_classified_noted_percent': 100 * fully_classified_noted / noted_issues,
                'paid_by_infos': paid_by_infos,
                'origin_infos': origin_infos,
            },
        )


class ProjectPendingIssuesView(ProjectSummaryHistoryMixin, IssuesMixin, DetailView):
    model = Project

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()

        user_id = None
        if 'user_id' in request.GET:
            try:
                user_id = int(request.GET['user_id'])
            except ValueError:
                pass

        issues = []
        last_version = self.get_future_version()
        if last_version:
            issues = self.get_pending_issues(last_version['modules'], user_id=user_id)

        return render(
            request,
            'projects/pending_issues.html',
            {'issues': issues},
        )


class ModulesView(TemplateView):
    template_name = 'projects/modules.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['modules'] = Module.objects.all().order_by('name')
        return context


def try_int(x):
    try:
        return int(x)
    except ValueError:
        return 0


version_re = re.compile(r'[\.\+-]d?e?v?')


def key_version(v1):
    return [try_int(x) for x in version_re.split(v1.version.version)]


class ModuleDeploymentsView(TemplateView):
    template_name = 'projects/deployments.html'

    def get_context_data(self, name, **kwargs):
        context = super().get_context_data(**kwargs)
        context['versions'] = []
        context['module'] = Module.objects.get(name=name)
        module = context['module']
        for service in InstalledService.objects.all():
            version = module.get_installed_version(service.platform, service.service)
            if version and version not in context['versions']:
                if version.version.version:
                    context['versions'].append(version)

        context['versions'].sort(key=key_version)
        return context


def module_deployments_json(request, name, **kwargs):
    response = HttpResponse(  # pylint: disable=http-response-with-content-type-json
        content_type='application/json'
    )
    installed_versions = []
    module = Module.objects.get(name=name)
    for service in InstalledService.objects.all():
        version = module.get_installed_version(service.platform, service.service)
        if version and version not in installed_versions:
            installed_versions.append(version)

    json.dump(
        [
            {
                'version': x.version.version,
                'project': x.service.platform.project.slug,
                'service': x.service.service.slug,
                'platform': x.service.platform.slug,
            }
            for x in installed_versions
        ],
        response,
    )
    return response


class IssuesSnippet(IssuesMixin, TemplateView):
    template_name = 'projects/issues_snippet.html'

    @csrf_exempt
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        modules = json.loads(self.request.read().decode('utf-8'))
        issues = self.get_issues(modules)
        context['issues'] = issues
        return context


class IssueInfoUpdate(FormView):
    form_class = IssueInfoForm
    template_name = 'projects/issue_info_form.html'
    success_url = '/'

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            raise PermissionDenied
        self.issue = Issue(kwargs['issue_id'])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['issue'] = self.issue
        return super().get_context_data(**kwargs)

    def get_form_kwargs(self):
        info, _ = IssueInfo.objects.get_or_create(issue_id=self.issue.id)
        kwargs = super().get_form_kwargs()
        kwargs['instance'] = info
        return kwargs

    def form_valid(self, form):
        form.save()
        self.issue.info = form.instance

        if not is_ajax(self.request):
            return super().form_valid(form)
        tmpl = template.loader.select_template(['projects/issue_fragment.html'])
        response = {
            'content': tmpl.render({'issue': self.issue}, self.request),
        }
        return JsonResponse(response)


def api_issues_json(request, *args, **kwargs):
    response = HttpResponse(  # pylint: disable=http-response-with-content-type-json
        content_type='application/json'
    )
    issue_id = kwargs.get('issue')
    data = get_issue_deployment_status(issue_id)
    json_str = json.dumps(data, indent=2)
    for variable in ('jsonpCallback', 'callback'):
        if variable in request.GET:
            response = HttpResponse(  # pylint: disable=http-response-with-content-type-json
                content_type='application/json'
            )

            json_str = '%s(%s);' % (request.GET[variable], json_str)
            break

    response.write(json_str)
    return response
