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

import os
import time
import urllib
from datetime import datetime
from slapos.certificate_authority.web.start_web import app, db
from slapos.certificate_authority.web.models import (Certificate,
                RevokeRequest, Revoke, CERT_STATUS_VALIDATED, CERT_STATUS_REVOKED,
                CERT_STATUS_PENDING, CERT_STATUS_REJECTED)


class CertificateTools:

  def signCertificate(self, req_file, cert_id):
    """
      Sign a certificate, cert_id is the name used by the user to download the cert
    """

    csr_dest = os.path.join(app.config.csr_dir, '%s.csr.pem' % cert_id)
    cert_name = '%s.cert.pem' % cert_id
    try:
      # Avoid signing two certificate at the same time (for unique serial)
      app.config.ca._lock()
      next_serial = app.config.ca.getNextSerial()
      output = os.path.join(app.config.cert_dir, '%s.cert.pem' % next_serial)

      # This should not happend normally
      assert not os.path.exists(output), output

      # Test that we can write on csr file
      os.close(os.open(req_file, os.O_RDWR | os.O_APPEND))
      app.config.ca.signCertificateRequest(req_file, output)
      cert = app.config.ca.freadX509(output)
      cert_db = Certificate(
          name=cert_name,
          serial=next_serial,
          filename='%s.cert.pem' % next_serial,
          common_name=cert.get_subject().CN,
          expire_after=datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ'),
          start_before=datetime.strptime(cert.get_notBefore(), '%Y%m%d%H%M%SZ')
      )
      db.session.add(cert_db)
      db.session.commit()
      os.rename(req_file, csr_dest)
    except Exception, e:
      if os.path.exists(output):
        try:
          # Revoke Certificate because problem occurred
          # No need to update CRL because the certificate was not used.
          app.config.ca.revokeCertificate(output)
        except:
          pass
        try:
          # Revoke certificate from database if exists
          db_cert = Certificate.query.filter(Certificate.serial == next_serial).first()
          if db_cert:
            db_cert.status = CERT_STATUS_REVOKED
            db_cert.comment = "Auto revoked due to error: %s" % str(e)
            db.session.commit()
        except:
          pass
      raise
    finally:
      app.config.ca._unlock()

    return cert_name

  def addRevokeRequest(self, cert, message):
    x509 = app.config.ca.readX509(cert)
    serial = self.getSerialToInt(x509)
    cert = Certificate.query.filter(
              Certificate.status == CERT_STATUS_VALIDATED
            ).filter(Certificate.serial == get_string_num(serial)).first()

    if not cert or cert.name != '%s.cert.pem' % hash_name:
      # This certificate not found or not match or was revoked
      return False

    # Create Request
    req = RevokeRequest(
        serial=serial,
        filename=cert.filename,
        common_name=cert.common_name,
        request_date=datetime.utcnow(),
        reason=message
      )
    db.session.add(req)
    db.session.commit()
    return True

  def cancelRevocationRequest(self, req_id):
    req = RevokeRequest.query.filter(
        RevokeRequest.id == req_id
        ).first()
    if req and req.status != CERT_STATUS_REJECTED:
      req.status = CERT_STATUS_REJECTED
      db.session.commit()
      return True
    return False

  def revokeCertificate(self, serial, request_id=None):
    request_entry = None
    if request_id:
      request_entry = RevokeRequest.query.filter(
                            RevokeRequest.id == request_id).first()
    if not request_entry:
      return False
    db_cert = Certificate.query.filter(Certificate.serial == serial).first()
    if db_cert:
      cert_file = os.path.join(app.config.cert_dir, db_cert.filename)
      if os.path.exists(cert_file):
        # XXX - This sequence should not fail
        # Call update CRL file right now
        app.config.ca.revokeCertificate(cert_file, update_crl=True)
        db_cert.status = CERT_STATUS_REVOKED
        if request_entry:
          request_entry.status = CERT_STATUS_VALIDATED
          rev = Revoke(comment="Validated from User request id=%s" % request_id,
                       serial=serial,
                       revoke_date=datetime.utcnow(),
                       revoke_request_id=request_id)
          db.session.add(rev)
        db.session.commit()
        return True

    return False

  def getFilenameFromHash(self, hash_name):
    cert = Certificate.query.filter(
              Certificate.status == CERT_STATUS_VALIDATED
            ).filter(Certificate.name == hash_name).first()
    if cert:
      return cert.filename

    return ""

  def getCertificateList(self, with_cacerts=True):
    if with_cacerts:
      ca_cert = app.config.ca.freadX509(app.config.ca.certificate)
      data_list = [
        {
          'index': 1,
          'filename': os.path.basename(app.config.ca.certificate),
          'name': os.path.basename(app.config.ca.certificate),
          'cn': ca_cert.get_subject().CN,
          'expiration_date': datetime.strptime(ca_cert.get_notAfter(),"%Y%m%d%H%M%SZ"),
          'start_date': datetime.strptime(ca_cert.get_notBefore(), "%Y%m%d%H%M%SZ")
        },
        {
          'index': 2,
          'filename': os.path.basename(app.config.ca.ca_crl),
          'name': os.path.basename(app.config.ca.ca_crl),
          'cn': "Certificate Revocation List",
          'expiration_date': '---',
          'start_date': '---',
        }
      ]
      index = 3
    else:
      data_list = []
      index = 1
    signed_cert_list = Certificate.query.filter(
        Certificate.status == CERT_STATUS_VALIDATED
        ).all()
    for signed_cert in signed_cert_list:
      data_list.append({
        'index': index,
        'serial': signed_cert.serial,
        'filename': signed_cert.filename,
        'name': signed_cert.name,
        'cn': signed_cert.common_name,
        'expiration_date': signed_cert.expire_after,
        'start_date': signed_cert.start_before,
      })
      index += 1

    return data_list

  def getRevokedCertificateList(self):
    index = 1
    data_list = []
    revoked_cert_list = Certificate.query.filter(
        Certificate.status == CERT_STATUS_REVOKED
        ).all()
    for revoked_cert in revoked_cert_list:
      data_list.append({
        'index': index,
        'serial': revoked_cert.serial,
        'filename': revoked_cert.filename,
        'name': revoked_cert.name,
        'cn': revoked_cert.common_name,
        'expiration_date': revoked_cert.expire_after,
        'start_date': revoked_cert.start_before,
      })
      index += 1

    return data_list

  def getRevocationRequestList(self):
    index = 1
    data_list = []
    revocation_list = RevokeRequest.query.order_by(
          RevokeRequest.status.asc()
        ).all()
    for request in revocation_list:
      frozen = False
      if request.status != CERT_STATUS_PENDING:
        frozen = True
      data_list.append({
        'index': index,
        'serial': request.serial,
        'filename': request.filename,
        'reason': request.reason,
        'cn': request.common_name,
        'request_date': request.request_date,
        'id': request.id,
        'status': request.status,
        'frozen': frozen,
      })
      index += 1

    return data_list

  def getCertificateRequestList(self):
    # XXX all files in app.config.req_dir folder are csr request from users
    data_list = []
    index = 1
    for file in os.listdir(app.config.req_dir):
      csr_file = os.path.join(app.config.req_dir, file)
      csr = app.config.ca.freadCertificateRequest(csr_file)
      data_list.append({
        'index': index,
        'filename': file,
        'filename_quote': urllib.quote(file),
        'key': file.rstrip('.csr.pem'),
        'cn': csr.get_subject().CN,
        'o': csr.get_subject().O,
        'ou': csr.get_subject().OU,
        'l': csr.get_subject().L,
        'st': csr.get_subject().ST,
        'c': csr.get_subject().C,
        'request_date': time.ctime(os.path.getmtime(csr_file))
      })
      index += 1
    return data_list

  def countCert(self):
    return Certificate.query.filter(
        Certificate.status == CERT_STATUS_VALIDATED
        ).count()

  def countCsr(self):
    return len(os.listdir(app.config.req_dir))

  def countRevokedCert(self):
    return Certificate.query.filter(
        Certificate.status == CERT_STATUS_REVOKED
        ).count()

  def countRevocationRequest(self):
    return RevokeRequest.query.filter(
        RevokeRequest.status == CERT_STATUS_PENDING
        ).count()

  def getSerialToInt(self, cert):
    return '{0:x}'.format(int(cert.get_serial_number()))

