# Copyright 2012 Mike Wakerly <opensource@hoho.com>
#
# This file is part of the Pykeg package of the Kegbot project.
# For more information on Pykeg or Kegbot, see http://kegbot.org/
#
# Pykeg is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Pykeg is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pykeg.  If not, see <http://www.gnu.org/licenses/>.

from pykeg import EPOCH

from pykeg.core import models
from pykeg.core.backend import KegbotBackend
from pykeg.core.cache import KegbotCache
from pykeg.web.api.util import is_api_request

from pykeg.plugin import util as plugin_util

from django.db import DatabaseError
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.http import Http404
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.response import SimpleTemplateResponse
from django.template import RequestContext
from django.utils import timezone

# TODO(mikey): rename me
ALLOWED_PATHS = (
    '/api/login/',
    '/api/get-api-key/',
    '/accounts/login/',
    '/admin/',
    '/media/',
    '/setup/',
)

def _path_allowed(path, kbsite):
  for p in ALLOWED_PATHS:
    if path.startswith(p):
      return True
  return False


class KegbotSiteMiddleware:
  ALLOWED_VIEW_MODULE_PREFIXES = (
      'pykeg.web.setup_wizard.',
  )
  def process_request(self, request):
    epoch = None
    request.need_setup = False
    request.need_upgrade = False

    try:
      request.kbsite = models.KegbotSite.objects.get(name='default')
      epoch = request.kbsite.epoch
    except (models.KegbotSite.DoesNotExist, DatabaseError), e:
      request.kbsite = None

    if not request.kbsite or not request.kbsite.is_setup:
      request.need_setup = True
    else:
      if not epoch or epoch < EPOCH:
        request.need_upgrade = True
      timezone.activate(request.kbsite.settings.timezone)
      request.plugins = dict((p.get_short_name(), p) for p in plugin_util.get_plugins())

    request.kbcache = KegbotCache()
    request.backend = KegbotBackend()
    return None

  def process_view(self, request, view_func, view_args, view_kwargs):
    for prefix in self.ALLOWED_VIEW_MODULE_PREFIXES:
      if view_func.__module__.startswith(prefix):
        return None

    if is_api_request(request):
      # API endpoints handle "setup required" differently.
      return None

    if request.need_setup:
      return self._setup_required(request)
    elif request.need_upgrade:
      return self._upgrade_required(request)

    return None

  def process_response(self, request, response):
    if request.method in ('POST', 'PUT', 'PATCH') and response.status_code < 400:
      # Invalidate cache on any successful change.
      # TODO(mikey): More granular cache invalidations.
      request.kbcache.update_generation()
    return response

  def _setup_required(self, request):
    return SimpleTemplateResponse('setup_wizard/setup_required.html',
        context=RequestContext(request), status=403)

  def _upgrade_required(self, request, current_epoch=None):
    context = RequestContext(request)
    context['current_epoch'] = current_epoch
    return SimpleTemplateResponse('setup_wizard/upgrade_required.html',
        context=context, status=403)


class SiteActiveMiddleware:
  """Middleware which throws 503s when KegbotSite.is_active is false."""
  def process_view(self, request, view_func, view_args, view_kwargs):
    if not hasattr(request, 'kbsite') or not request.kbsite:
      return None
    kbsite = request.kbsite

    # We have a KegbotSite, and that site is active: nothing to do.
    if kbsite.is_active:
      return None

    # If the request is for a whitelisted path, allow it.
    if _path_allowed(request.path, kbsite):
      return None

    # Allow staff/superusers access if inactive.
    if request.user.is_staff or request.user.is_superuser:
      return None

    return HttpResponse('Site temporarily unavailable', status=503)

class HttpHostMiddleware:
  """Middleware which checks a dynamic version of settings.ALLOWED_HOSTS."""
  def process_request(self, request):
    if not getattr(request, 'kbsite', None):
      return None

    host = request.get_host()
    allowed_hosts_str = request.kbsite.settings.allowed_hosts
    if allowed_hosts_str:
      host_patterns = allowed_hosts_str.strip().split()
    else:
      host_patterns = []
    valid = HttpHostMiddleware.validate_host(host, host_patterns)

    if not valid:
      message =  "Invalid HTTP_HOST header (you may need to change Kegbot's ALLOWED_HOSTS setting): %s" % host
      if request.user.is_superuser or request.user.is_staff:
        messages.warning(request, message)
      else:
        raise SuspiciousOperation(message)

  @classmethod
  def validate_host(cls, host, allowed_hosts):
    """Clone of django.http.request.validate_host.

    Local differences: treats an empty `allowed_hosts` list as a pass.
    """
    # Validate only the domain part.
    if not allowed_hosts:
      return True

    if host[-1] == ']':
        # It's an IPv6 address without a port.
        domain = host
    else:
        domain = host.rsplit(':', 1)[0]

    for pattern in allowed_hosts:
      pattern = pattern.lower()
      match = (
          pattern == '*' or
          pattern.startswith('.') and (
              domain.endswith(pattern) or domain == pattern[1:]
              ) or
          pattern == domain
          )
      if match:
          return True

    return False


class PrivacyMiddleware:
  """Enforces site privacy settings.

  Must be installed after ApiRequestMiddleware (in request order) to
  access is_kb_api_request attribute.
  """
  def process_view(self, request, view_func, view_args, view_kwargs):
    if not hasattr(request, 'kbsite'):
      return None
    elif _path_allowed(request.path, request.kbsite):
      return None
    elif request.is_kb_api_request:
      # api.middleware will enforce access requirements.
      return None

    privacy = request.kbsite.settings.privacy

    if privacy == 'public':
      return None
    elif privacy == 'staff':
      if not request.user.is_staff:
        return SimpleTemplateResponse('kegweb/staff_only.html',
            context=RequestContext(request), status=401)
      return None
    elif privacy == 'members':
      if not request.user.is_authenticated or not request.user.is_active:
        return SimpleTemplateResponse('kegweb/members_only.html',
            context=RequestContext(request), status=401)
      return None

    return HttpResponse('Server misconfigured, unknown privacy setting:%s' % privacy, status=500)

