# -*- coding: utf-8 -*-


import os
import sys
import traceback
from OpenSSL import crypto
import uuid
from datetime import datetime
from flask import (Flask, session, request, redirect, url_for, render_template,
                    jsonify, session, abort, send_file, flash, g, Response)
from slapos.certificate_authority.web.start_web import app, db
from slapos.certificate_authority.web.tools.users import find_user, find_or_create_user
from slapos.certificate_authority.web.tools.tools  get_string_num
from slapos.certificate_authority.web.tools.error import Error as error
from slapos.certificate_authority.web.tools.certificate import CertificateTools
from flask_user import login_required, current_user
from flask_login import logout_user  #, login_user, current_user, login_required
from slapos.certificate_authority.web.models import (UserProfileForm, Certificate)
from slapos.runner.utils import tail
from functools import wraps


cert_tools = CertificateTools()


def authenticated_method(func):
  """ This decorator ensures that the current user is logged in before calling the actual view.
      Abort with 401 when the user is not logged in."""
  @wraps(func)
  def decorated_view(*args, **kwargs):
      # User must be authenticated
      auth = request.authorization
      if not auth:
        return abort(401)
      elif not Users.check_authentication(auth.username, auth.password):
        return abort(401)
      # Call the actual view
      return func(*args, **kwargs)
  return decorated_view


@app.errorhandler(401)
def error401(error):
  if error.description is None:
    message = {
      'code': 401,
      'name': 'Unauthorized',
      'message': "Authenticate."
    }
  else:
    message = error.description
  response = jsonify(message)

  response.status_code = 401
  response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'

  return response

@app.errorhandler(403)
def error403(error):
  if error.description is None:
    message = {
      'code': 404,
      'name': 'Forbidden',
      'message': 'Forbidden. Your are not allowed to access %s' % request.url,
    }
  else:
    message = error.description
  response = jsonify(message)
  response.status_code = 404

  return response

@app.errorhandler(404)
def error404(error):
  if error.description is None:
    message = {
      'code': 404,
      'name': 'NotFound',
      'message': 'Resource not found: ' + request.url,
    }
  else:
    message = error.description
  response = jsonify(message)
  response.status_code = 404

  return response

@app.errorhandler(400)
def error400(error):
  if error.description is None:
    message = {
      'code': 400,
      'name': 'BadRequest',
      'message': 'The request could not be understood by the server, you probably provided wrong parameters.'
    }
  else:
    message = error.description
  response = jsonify(message)
  response.status_code = 400

  return response



@app.before_request
def before_request():
  if not request.path.startswith('/admin/'):
    return
  if request.path == '/admin/configure' or request.path == '/admin/setpassword':
    # check if password file exists, if yes go to index
    if find_user('admin'):
      return redirect(url_for('home'))
    return
  # XXX - using hard username
  if not find_user('admin'):
    return redirect(url_for('configure'))
  g.user = current_user



# Routes for certificate Authority

@app.route('/crl', methods=['GET'])
def get_crl():

  if os.path.exists(app.config.ca.ca_crl):
    return send_file(app.config.ca.ca_crl,
                    attachment_filename=os.path.basename(app.config.ca.ca_crl),
                    as_attachment=True)
  else:
    return abort(404)

@app.route('/csr/<string:csr_id>', methods=['GET'])
def get_csr(csr_id):
  csr_file = os.path.join(app.config.req_dir, csr_id)
  if os.path.exists(csr_file) and os.path.isfile(csr_file):
    return send_file(csr_file,
                attachment_filename=os.path.basename(csr_file),
                as_attachment=True)
  else:
    return abort(404)

@app.route('/csr', methods=['PUT'])
def request_cert():
  csr_content = request.form.get('csr', '').encode('utf-8')
  if not csr_content:
    return abort(400, error.MISSING_PARAM)

  try:
    # check if a valid csr content
    req = app.config.ca.readCertificateRequest(csr_content)
  except crypto.Error, e:
    return abort(400, error.CSR_FORMAT)

  return do_requestcert(csr_content)

@app.route('/csr/<string:csr_id>', methods=['DELETE'])
@authenticated_method
def remove_csr(csr_id):
  req_file = os.path.join(app.config.req_dir, csr_id)
  if os.path.exists(req_file) and os.path.isfile(req_file):
    os.unlink(req_file)
  else:
    return abort(404)

@app.route('/crt/<string:cert_id>', methods=['GET'])
def get_crt(cert_id):
  cacert_id = os.path.basename(app.config.ca.certificate)
  if cert_id == cacert_id:
    cert_file = app.config.ca.certificate
  else:
    cert_filename = cert_tools.getFilenameFromHash(name)
    cert_file = os.path.join(app.config.cert_dir, cert_filename)

  if os.path.exists(cert_file) and os.path.isfile(cert_file):
    return send_file(cert_file,
                      attachment_filename=os.path.basename(cert_file),
                      as_attachment=True)
  else:
    return abort(404, cert_id)

@app.route('/crt', methods=['PUT'])
@authenticated_method
def sign_cert():
  key = request.form.get('key', '').encode('utf-8')
  if not key:
    return abort(400)

  return signcert(key)

@app.route('/crt/renew', methods=['PUT'])
def renew_cert(serial):
  """
  this method is used to renew expired certificate.
  should recieve csr and old cert serial.
  """
  digest = request.form.get('digest', '')
  signature = request.form.get('signature', '')
  # payload: a Json string containing certificate to revoke and reason msg
  payload_string = request.form.get('payload', '')
  if not digest or not signature or not payload_string:
    # Bad parameters
    return abort(400)

  try:
    payload = json.loads(payload_string)
  except ValueError, exc:
    return abort(400, error.JSON_FORMAT, str(exc))

  if not payload.has_key('cert') and payload.has_key('csr'):
    return abort(400)

  if not digest in app.config.CA_DIGEST_LIST:
    return abort(400, error.INVALID_DIGEST, '%r not in allowed signature hash set: %s' % (digest, app.config.CA_DIGEST_LIST))

  try:
    req = app.config.ca.readCertificateRequest(payload['csr'])
  except crypto.Error, e:
    return abort(400, error.CSR_FORMAT)

  try:
    cert = app.config.ca.readX509(payload['cert'])
  except crypto.Error, e:
    return abort(400, error.CERT_FORMAT)

  if not app.config.ca.verifyData(payload['cert'], signature, payload, digest):
    return abort(400, Error.SIGNATURE_VERIFICATION)
  
  if req.get_subject().CN != cert.get_subject().CN:
    return abort(400, error.CSR_CONTENT_MISMATCH)

  return do_requestcert(payload['csr'])

@app.route('/crt/revoke', methods=['PUT'])
def request_revoke_crt():
  """
    Revoke method required certificat: serial(Hex), name and reason
    'name' is used here to add more verification
    'reason' is a message that say why this certificate should be revoked
  """
  digest = request.form.get('digest', '')
  signature = request.form.get('signature', '')
  # payload: a Json string containing certificate to revoke and reason msg
  payload_string = request.form.get('payload', '')
  if not digest or not signature or not payload_string:
    # Bad parameters
    return abort(400)

  try:
    payload = json.loads(payload_string)
  except ValueError:
    return abort(400)

  if not payload.has_key('cert') and payload.has_key('reason'):
    return abort(400)

  if not digest in app.config.CA_DIGEST_LIST:
    return abort(400)

  try:
    cert = app.config.ca.readX509(payload['cert'])
  except crypto.Error, e:
    return abort(400, error.CERT_FORMAT)

  if not app.config.ca.verifyData(payload['cert'], signature, payload, digest):
    return abort(400, Error.SIGNATURE_VERIFICATION)

  if cert_tools.addRevokeRequest(payload['cert'], name, payload['reason']):
    message = {
      "code": 201,
      "name": "Created",
      "message": "Content created - Certificate was revoked"
    }

    response = Response(message,
                        status=201,
                        mimetype='application/json')
    return response
  else:
    return abort(404)




def do_requestcert(csr):
  key = str(uuid.uuid1())
  csr_filename = '%s.csr.pem' % key
  csr_file = os.path.join(app.config.req_dir, csr_filename)

  fd = os.open(csr_file,
               os.O_CREAT|os.O_WRONLY|os.O_EXCL|os.O_TRUNC,
               0640)
  try:
    os.write(fd, csr)
  finally:
    os.close(fd)

  message = {
    "code": 201,
    "name": "Created",
    "message": "Content created - Certificate Signing Request was accepted"
  }

  response = Response(message,
                      status=201,
                      mimetype='application/json')
  response.headers['Location'] = url_for('get_csr', _external=True, cert_id=csr_filename)

  return response

def signcert(csr_key, redirect_to=''):
  req_file = os.path.join(app.config.req_dir, csr_key)
  if os.path.exists(req_file) and os.path.isfile(req_file):
    cert_id = csr_key.split('.')[0]
    # cert_id = csr_key[:-8]
    try:
      cert_name = cert_tools.signCertificate(req_file, cert_id)
    except Exception, e:
      # XXX - need to check this
      raise
      # return abort(501, str(e))
  else:
    return abort(404)

  # XXX - to remove
  flash('Certificate is signed!', 'success')
  if redirect_to:
    return redirect(url_for(redirect_to))

  message = {
    "code": 201,
    "name": "Created",
    "message": "Content created - Certificate was signed"
  }

  response = Response(message,
                      status=201,
                      mimetype='application/json')
  response.headers['Location'] = url_for('get_crt', _external=True, cert_id=cert_name)

  return response


#Manage routes (Authentication required) - Flask APP

@app.route('/')
def index():
  # page to list certificates, also connection link
  data_list = cert_tools.getCertificateList()
  return render_template("index.html", data_list=data_list)

@app.route('/admin/logout')
def logout():
    logout_user()
    return redirect(url_for('home'))

@app.route('/admin/configure', methods=['GET'])
def configure():
  return render_template("configure.html")

@app.route('/admin/setpassword', methods=['POST'])
def setpassword():
  username = 'admin'
  password = request.form.get('password', '').encode('utf-8')
  if not password:
    return abort(412, "password not set or empty")

  find_or_create_user(
    "Admin",
    "admin",
    "admin@example.com",
    username,
    password)
  logout_user()
  return redirect(url_for('home'))

@app.route('/admin/manage', methods=['GET'])
@login_required
def manage_home():
  data_list = cert_tools.getCertificateRequestList()
  c = dict(
    cert=get_string_num(cert_tools.countCert()),
    csr=get_string_num(cert_tools.countCsr()),
    crl=get_string_num(cert_tools.countRevokedCert()),
    crr=get_string_num(cert_tools.countRevocationRequest())
  )
  return render_template('manage_page.html', data_list=data_list, counter=c)

@app.route('/admin/profile', methods=['GET', 'POST'])
@login_required
def user_profile():
    form = UserProfileForm(request.form, obj=current_user)
    if request.method == 'POST' and form.validate():
        # Copy form fields to user_profile fields
        del form.username  # revove username from recieved form
        form.populate_obj(current_user)
        db.session.commit()

        # Redirect to home page
        return redirect(url_for('home'))

    return render_template('user_profile.html',
                           form=form)

@app.route('/admin/viewcert', methods=['GET'])
@login_required
def viewcert():
  type_dict = {
    'cacert': os.path.dirname(app.config.ca.certificate),
    'X509': app.config.cert_dir,
    'req': app.config.req_dir,
    'crl': app.config.crl_dir,
  }
  filename = request.args.get('name')
  request_type = request.args.get('type')
  content = cert_type = ""
  name = "Requested file not found!"
  if filename and request_type in type_dict.keys():
    path = os.path.join(type_dict[request_type], filename)
    if os.path.exists(path) and os.path.isfile(path):
      name = filename
      if request_type in ['X509', 'cacert']:
        cert_type = 'Certificate'
        content = app.config.ca.getX509Text(app.config.ca.openssl_binary, path)
      elif request_type == 'req':
        cert_type = 'CSR'
        content = app.config.ca.getCsrText(app.config.ca.openssl_binary, path)
      elif request_type == 'crl':
        cert_type = 'CRL'
        content = app.config.ca.getCrlText(app.config.ca.openssl_binary, path)

  return render_template('view_cert.html', content=content, name=name, cert_type=cert_type)

@app.route('/admin/signcert_web', methods=['GET'])
@login_required
def do_signcert_web():
  filename = request.args.get('name', '').encode('utf-8')
  if not filename or len(filename) < 9:
    return abort(412)
  return signcert(filename[:-8], 'manage')

@app.route('/admin/deletecsr', methods=['GET'])
@login_required
def deletecsr():
  """
    Delete certificate signature request file
  """
  filename = request.args.get('name', '').encode('utf-8')
  if not filename:
    return abort(412)
  req_file = os.path.join(app.config.req_dir, filename)
  if os.path.exists(req_file):
    os.unlink(req_file)
    flash('CSR file "%s" removed!' % filename, 'success')
    return redirect(url_for('manage'))
  else:
    return abort(404)

@app.route('/admin/signed_certs', methods=['GET'])
@login_required
def signed_certificate_list():
  # page to list certificates, also connection link
  data_list = cert_tools.getCertificateList(False)
  return render_template("signed_certs.html", data_list=data_list)

@app.route('/admin/revoked_certs', methods=['GET'])
@login_required
def revoked_certificate_list():
  # page to list certificates, also connection link
  data_list = cert_tools.getRevokedCertificateList()
  return render_template("revoked_certs.html", data_list=data_list)

@app.route('/admin/revoke_cert', methods=['GET', 'POST'])
@login_required
def revoke_cert():
  redirect_url = 'signed_certs'
  serial = request.args.get('serial', '')
  if not serial:
    return abort(412, "serial is invalid")
  request_id = request.args.get('request_id', None)
  if request_id:
    redirect_url = 'revocation_requests'

  if cert_tools.revokeCertificate(serial, request_id):
    flash('Certificate was revoked!', 'success')
    return redirect(url_for(redirect_url))

  return abort(404)

@app.route('/admin/revocation_requests', methods=['GET'])
@login_required
def revocation_request_list():
  data_list = cert_tools.getRevocationRequestList()
  return render_template("revocation_requests.html", data_list=data_list)

@app.route('/admin/reject_revocation_request', methods=['GET'])
@login_required
def reject_revocation_request():
  req_id = request.args.get('request_id', '')
  if not req_id:
    return abort(412)
  if cert_tools.cancelRevocationRequest(req_id):
    flash('Revocation Request Cancelled!', 'success')
  else:
    flash('Revocation Request not found!', 'info')
  return redirect(url_for('revocation_requests'))


@app.route('/admin/view_logs', methods=['GET'])
@login_required
def view_logs():
  content = ""
  full_log = request.args.get('full', '')
  if os.path.exists(app.config.log_file):
    with open(app.config.log_file) as f:
      if full_log == 'true':
        content = f.read()
      else:
        content = tail(f, 500)
  return render_template("view_logs.html", content=content, filename=app.config.log_file)

@app.route('/admin/generate_crl', methods=['GET'])
@login_required
def generate_crl():
  app.config.ca.genCertificateRevocationList()
  return redirect(url_for('home'))


app.add_url_rule('/', 'home', index)
app.add_url_rule('/admin/manage', 'manage', manage_home)
app.add_url_rule('/admin/signed_certs', 'signed_certs', signed_certificate_list)
app.add_url_rule('/admin/revocation_requests', 'revocation_requests', revocation_request_list)