"""Module for manipulating and installing Configuration Profiles."""

import plistlib
import tempfile
import uuid
from OpenSSL import crypto
from . import gmacpyutil
from . import certs
from . import defaults

CMD_PROFILES = '/usr/bin/profiles'

PAYLOADKEYS_IDENTIFIER = 'PayloadIdentifier'
PAYLOADKEYS_DISPLAYNAME = 'PayloadDisplayName'
PAYLOADKEYS_TYPE = 'PayloadType'
PAYLOADKEYS_DESCRIPTION = 'PayloadDescription'
PAYLOADKEYS_ORG = 'PayloadOrganization'
PAYLOADKEYS_SCOPE = 'PayloadScope'
PAYLOADKEYS_CONTENT = 'PayloadContent'
PAYLOADKEYS_UUID = 'PayloadUUID'
PAYLOADKEYS_ENABLED = 'PayloadEnabled'
PAYLOADKEYS_VERSION = 'PayloadVersion'

NETWORK_PROFILE_ID = defaults.NETWORK_PROFILE_ID
ORGANIZATION_NAME = defaults.ORGANIZATION_NAME


class Error(Exception):
  """Base error class."""


class ProfileSaveError(Error):
  """Error saving configuration profile."""


class ProfileInstallationError(Error):
  """Error installing configuration profile."""


class ProfileValidationError(Error):
  """Error validating configuration profile."""


class PayloadValidationError(Error):
  """Error validating payload."""


class CertificateError(Error):
  """Error adding a certificate to a network configuration profile."""


def GenerateUUID(payload_id):
  """Generates a UUID for a given PayloadIdentifier.

  This function will always generate the same UUID for a given identifier.

  Args:
    payload_id: str, a payload identifier string (reverse-dns style).

  Returns:
    uuid: str, a valid UUID based on the payload ID.
  """
  return str(uuid.uuid5(uuid.NAMESPACE_DNS, payload_id)).upper()


def ValidatePayload(payload):
  """Validate the payload includes all required keys.

  Will automatically add the following keys if they do not already exist:
    PayloadUUID, PayloadEnabled, PayloadVersion

  Args:
    payload: dict, the payload to validate.

  Raises:
    PayloadValidationError: the payload is missing a required key.
  """
  required_keys = [PAYLOADKEYS_IDENTIFIER,
                   PAYLOADKEYS_DISPLAYNAME,
                   PAYLOADKEYS_TYPE]

  for key in required_keys:
    if key not in payload:
      raise PayloadValidationError('Required key (%s) missing.' % key)

  if PAYLOADKEYS_UUID not in payload:
    payload[PAYLOADKEYS_UUID] = GenerateUUID(payload[PAYLOADKEYS_IDENTIFIER])

  if PAYLOADKEYS_ENABLED not in payload:
    payload[PAYLOADKEYS_ENABLED] = True

  if PAYLOADKEYS_VERSION not in payload:
    payload[PAYLOADKEYS_VERSION] = 1


class Profile(object):
  """Represents a configuration profile which can be installed."""

  def __init__(self):
    self._profile = plistlib.Plist()
    self.Set(PAYLOADKEYS_CONTENT, [])

  def __str__(self):
    return self.Get(PAYLOADKEYS_DISPLAYNAME)

  def Get(self, key):
    return self._profile.get(key)

  def Set(self, key, value):
    self._profile[key] = value

  def _ValidateProfile(self):
    """Validate the profile and all payloads are valid.

    Raises:
      ProfileValidationError: the profile data was not valid.
    """
    required_keys = [PAYLOADKEYS_DISPLAYNAME,
                     PAYLOADKEYS_IDENTIFIER,
                     PAYLOADKEYS_ORG,
                     PAYLOADKEYS_SCOPE,
                     PAYLOADKEYS_TYPE]
    for key in required_keys:
      if not self.Get(key):
        raise ProfileValidationError('Required key (%s) missing.' % key)

    if not self.Get(PAYLOADKEYS_UUID):
      self.Set(PAYLOADKEYS_UUID, GenerateUUID(self.Get(PAYLOADKEYS_IDENTIFIER)))

    if not self.Get(PAYLOADKEYS_VERSION):
      self.Set(PAYLOADKEYS_VERSION, 1)

    if len(self.Get(PAYLOADKEYS_CONTENT)) < 1:
      raise ProfileValidationError('Profile has no payloads.')

  def AddPayload(self, payload):
    """Adds a new payload to the PayloadContent dict.

    Args:
      payload: dict, dictionary of payload data.

    Raises:
      PayloadValidationError: payload could not be validated.
    """
    ValidatePayload(payload)
    self.Get(PAYLOADKEYS_CONTENT).append(payload)

  def Save(self, path):
    """Save the profile to disk.

    Args:
      path: str, the path to save the profile to.

    Raises:
      ProfileValidationError: profile data was not valid.
      ProfileSaveError: profile could not be saved.
    """
    self._ValidateProfile()
    try:
      plistlib.writePlist(self._profile, path)
    except (IOError, TypeError) as e:
      raise ProfileSaveError('The profile could not be saved: %s' % e)

  def Install(self, sudo_password=None):
    """Install the profile.

    Args:
      sudo_password: str, the password to use for installing the profile.

    Raises:
      ProfileInstallationError: profile failed to install.
      ProfileValidationError: profile data was not valid.
      ProfileSaveError: profile could not be saved.
    """
    self._ValidateProfile()

    with tempfile.NamedTemporaryFile(suffix='.mobileconfig',
                                     prefix='profile_') as f:
      temp_file = f.name

      self.Save(temp_file)
      command = [CMD_PROFILES, '-I', '-F', temp_file]

      try:
        (stdout, stderr, status) = gmacpyutil.RunProcess(
            command, sudo=sudo_password, sudo_password=sudo_password)
      except gmacpyutil.GmacpyutilException as e:
        raise ProfileInstallationError('Profile installation failed!\n%s' % e)

      if status:
        raise ProfileInstallationError('Profile installation failed!\n'
                                       '%s, %s, %s' % (stdout, stderr, status))


class NetworkProfile(Profile):
  """Represents a configuration profile containing network settings."""

  def __init__(self, username):
    """Initializes a network configuration profile with no payloads.

    Args:
      username: str, the username associated with this profile.
    """
    super(NetworkProfile, self).__init__()

    self._username = username
    self._auth_cert = None
    self._anchor_certs = []
    self._trusted_servers = []

    self.Set(PAYLOADKEYS_DISPLAYNAME, 'Network Profile (%s)' % self._username)
    self.Set(PAYLOADKEYS_DESCRIPTION, 'Network authentication settings')
    self.Set(PAYLOADKEYS_IDENTIFIER, NETWORK_PROFILE_ID)
    self.Set(PAYLOADKEYS_ORG, ORGANIZATION_NAME)
    self.Set(PAYLOADKEYS_SCOPE, ['System', 'User'])
    self.Set(PAYLOADKEYS_TYPE, 'Configuration')
    self.Set(PAYLOADKEYS_CONTENT, [])

  def _GenerateID(self, suffix):
    """Generates a unique PayloadIdentifier for a given suffix."""
    return '%s.%s' % (self.Get(PAYLOADKEYS_IDENTIFIER), suffix)

  def AddMachineCertificate(self, certificate, private_key):
    """Adds a machine certificate payload to the profile.

    Args:
      certificate: str, PEM-formatted certificate.
      private_key: str, PEM-formatted private key.

    Raises:
      CertificateError: there was an error processing the certificate/key
    """
    try:
      cert = certs.Certificate(certificate)

      pkcs12 = crypto.PKCS12Type()
      pkcs12.set_certificate(crypto.load_certificate(
          crypto.FILETYPE_PEM, certificate))
      pkcs12.set_privatekey(crypto.load_privatekey(
          crypto.FILETYPE_PEM, private_key))
    except (certs.CertError, crypto.Error) as e:
      raise CertificateError(e)

    payload = {PAYLOADKEYS_IDENTIFIER: self._GenerateID('machine_cert'),
               PAYLOADKEYS_TYPE: 'com.apple.security.pkcs12',
               PAYLOADKEYS_DISPLAYNAME: cert.subject_cn,
               'Password': cert.osx_fingerprint}

    try:
      payload[PAYLOADKEYS_CONTENT] = plistlib.Data(
          pkcs12.export(cert.osx_fingerprint))
    except crypto.Error as e:
      raise CertificateError(e)

    # Validate payload to generate its UUID
    ValidatePayload(payload)
    self._auth_cert = payload.get(PAYLOADKEYS_UUID)
    self.AddPayload(payload)

  def AddAnchorCertificate(self, certificate):
    """Adds a certificate payload to the profile for server identification.

    Args:
      certificate: str, PEM-formatted certificate.

    Raises:
      CertificateError: there was an error processing the certificate
    """
    try:
      cert = certs.Certificate(certificate)
    except certs.CertError as e:
      raise CertificateError(e)

    payload = {PAYLOADKEYS_IDENTIFIER: self._GenerateID(cert.osx_fingerprint),
               PAYLOADKEYS_TYPE: 'com.apple.security.pkcs1',
               PAYLOADKEYS_DISPLAYNAME: cert.subject_cn,
               PAYLOADKEYS_CONTENT: plistlib.Data(certificate)}

    # Validate payload to generate its UUID
    ValidatePayload(payload)
    self._anchor_certs.append(payload.get(PAYLOADKEYS_UUID))
    self.AddPayload(payload)

  def AddTrustedServer(self, server_dn):
    """Adds a server to the trusted servers list.

    Args:
      server_dn: str, a server's DNS name.
    """
    self._trusted_servers.append(server_dn)

  def AddNetworkPayload(self, ssid):
    """Adds a network payload to the profile.

    If the SSID specified is 'wired' a wired payload will be created.

    NOTE: If you intend to use |AddMachineCertifcate|, |AddAnchorCertifcate| or
    |AddTrustedServer| then you must call them before this method or they won't
    take effect.

    Args:
      ssid: str, the SSID to create a payload for.
    """
    payload = {'AutoJoin': True,
               'SetupModes': ['System', 'User'],
               'PayloadCertificateUUID': self._auth_cert}

    if ssid == 'wired':
      payload[PAYLOADKEYS_DISPLAYNAME] = 'Wired'
      payload[PAYLOADKEYS_IDENTIFIER] = self._GenerateID('wired')
      payload[PAYLOADKEYS_TYPE] = 'com.apple.firstactiveethernet.managed'
      payload['EncryptionType'] = 'Any'
      payload['Interface'] = 'FirstActiveEthernet'
    else:
      payload[PAYLOADKEYS_DISPLAYNAME] = ssid
      payload[PAYLOADKEYS_IDENTIFIER] = self._GenerateID('ssid.%s' % ssid)
      payload[PAYLOADKEYS_TYPE] = 'com.apple.wifi.managed'
      payload['EncryptionType'] = 'WPA'
      payload['Interface'] = 'BuiltInWireless'
      payload['SSID_STR'] = ssid

    eap_client_config = {}
    eap_client_config['AcceptEAPTypes'] = [13,]
    eap_client_config['PayloadCertificateAnchorUUID'] = self._anchor_certs
    eap_client_config['TLSTrustedServerNames'] = self._trusted_servers
    eap_client_config['TLSAllowTrustExceptions'] = False
    payload['EAPClientConfiguration'] = eap_client_config

    self.AddPayload(payload)