#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = '''
---
module: oneandone_firewall_policy
short_description: Configure 1&1 firewall policy
description:
  - Create, remove, reconfigure, update firewall policies.
    This module has a dependency on 1and1 >= 1.0.
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  state:
    description:
      - Define a firewall policy state to create, remove, or update.
    required: false
    type: str
    default: 'present'
    choices: [ "present", "absent", "update" ]
  auth_token:
    description:
      - Authenticating API token provided by 1&1.
    type: str
  api_url:
    description:
      - Custom API URL. Overrides the
        ONEANDONE_API_URL environment variable.
    type: str
    required: false
  name:
    description:
      - Firewall policy name used with present state. Used as identifier (id or name) when used with absent state.
        maxLength=128
    type: str
  firewall_policy:
    description:
      - The identifier (id or name) of the firewall policy used with update state.
    type: str
  rules:
    description:
      - A list of rules that will be set for the firewall policy.
        Each rule must contain protocol parameter, in addition to three optional parameters
        (port_from, port_to, and source)
    type: list
    elements: dict
    default: []
  add_server_ips:
    description:
      - A list of server identifiers (id or name) to be assigned to a firewall policy.
        Used in combination with update state.
    type: list
    elements: str
    required: false
    default: []
  remove_server_ips:
    description:
      - A list of server IP ids to be unassigned from a firewall policy. Used in combination with update state.
    type: list
    elements: str
    required: false
    default: []
  add_rules:
    description:
      - A list of rules that will be added to an existing firewall policy.
        It is syntax is the same as the one used for rules parameter. Used in combination with update state.
    type: list
    elements: dict
    required: false
    default: []
  remove_rules:
    description:
      - A list of rule ids that will be removed from an existing firewall policy. Used in combination with update state.
    type: list
    elements: str
    required: false
    default: []
  description:
    description:
      - Firewall policy description. maxLength=256
    type: str
    required: false
  wait:
    description:
      - wait for the instance to be in state 'running' before returning
    required: false
    default: true
    type: bool
  wait_timeout:
    description:
      - how long before wait gives up, in seconds
    type: int
    default: 600
  wait_interval:
    description:
      - Defines the number of seconds to wait when using the _wait_for methods
    type: int
    default: 5

requirements:
  - "1and1"

author:
  -  "Amel Ajdinovic (@aajdinov)"
  -  "Ethan Devenport (@edevenport)"
'''

EXAMPLES = '''
- name: Create a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    name: ansible-firewall-policy
    description: Testing creation of firewall policies with ansible
    rules:
     -
       protocol: TCP
       port_from: 80
       port_to: 80
       source: 0.0.0.0
    wait: true
    wait_timeout: 500

- name: Destroy a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    state: absent
    name: ansible-firewall-policy

- name: Update a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    state: update
    firewall_policy: ansible-firewall-policy
    name: ansible-firewall-policy-updated
    description: Testing creation of firewall policies with ansible - updated

- name: Add server to a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    firewall_policy: ansible-firewall-policy-updated
    add_server_ips:
     - server_identifier (id or name)
     - server_identifier #2 (id or name)
    wait: true
    wait_timeout: 500
    state: update

- name: Remove server from a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    firewall_policy: ansible-firewall-policy-updated
    remove_server_ips:
     - B2504878540DBC5F7634EB00A07C1EBD (server's IP id)
    wait: true
    wait_timeout: 500
    state: update

- name: Add rules to a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    firewall_policy: ansible-firewall-policy-updated
    description: Adding rules to an existing firewall policy
    add_rules:
     -
       protocol: TCP
       port_from: 70
       port_to: 70
       source: 0.0.0.0
     -
       protocol: TCP
       port_from: 60
       port_to: 60
       source: 0.0.0.0
    wait: true
    wait_timeout: 500
    state: update

- name: Remove rules from a firewall policy
  community.general.oneandone_firewall_policy:
    auth_token: oneandone_private_api_key
    firewall_policy: ansible-firewall-policy-updated
    remove_rules:
     - rule_id #1
     - rule_id #2
     - ...
    wait: true
    wait_timeout: 500
    state: update
'''

RETURN = '''
firewall_policy:
    description: Information about the firewall policy that was processed
    type: dict
    sample: '{"id": "92B74394A397ECC3359825C1656D67A6", "name": "Default Policy"}'
    returned: always
'''

import os
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.oneandone import (
    get_firewall_policy,
    get_server,
    OneAndOneResources,
    wait_for_resource_creation_completion
)

HAS_ONEANDONE_SDK = True

try:
    import oneandone.client
except ImportError:
    HAS_ONEANDONE_SDK = False


def _check_mode(module, result):
    if module.check_mode:
        module.exit_json(
            changed=result
        )


def _add_server_ips(module, oneandone_conn, firewall_id, server_ids):
    """
    Assigns servers to a firewall policy.
    """
    try:
        attach_servers = []

        for _server_id in server_ids:
            server = get_server(oneandone_conn, _server_id, True)
            attach_server = oneandone.client.AttachServer(
                server_id=server['id'],
                server_ip_id=next(iter(server['ips'] or []), None)['id']
            )
            attach_servers.append(attach_server)

        if module.check_mode:
            if attach_servers:
                return True
            return False

        firewall_policy = oneandone_conn.attach_server_firewall_policy(
            firewall_id=firewall_id,
            server_ips=attach_servers)
        return firewall_policy
    except Exception as e:
        module.fail_json(msg=str(e))


def _remove_firewall_server(module, oneandone_conn, firewall_id, server_ip_id):
    """
    Unassigns a server/IP from a firewall policy.
    """
    try:
        if module.check_mode:
            firewall_server = oneandone_conn.get_firewall_server(
                firewall_id=firewall_id,
                server_ip_id=server_ip_id)
            if firewall_server:
                return True
            return False

        firewall_policy = oneandone_conn.remove_firewall_server(
            firewall_id=firewall_id,
            server_ip_id=server_ip_id)
        return firewall_policy
    except Exception as e:
        module.fail_json(msg=str(e))


def _add_firewall_rules(module, oneandone_conn, firewall_id, rules):
    """
    Adds new rules to a firewall policy.
    """
    try:
        firewall_rules = []

        for rule in rules:
            firewall_rule = oneandone.client.FirewallPolicyRule(
                protocol=rule['protocol'],
                port_from=rule['port_from'],
                port_to=rule['port_to'],
                source=rule['source'])
            firewall_rules.append(firewall_rule)

        if module.check_mode:
            firewall_policy_id = get_firewall_policy(oneandone_conn, firewall_id)
            if (firewall_rules and firewall_policy_id):
                return True
            return False

        firewall_policy = oneandone_conn.add_firewall_policy_rule(
            firewall_id=firewall_id,
            firewall_policy_rules=firewall_rules
        )
        return firewall_policy
    except Exception as e:
        module.fail_json(msg=str(e))


def _remove_firewall_rule(module, oneandone_conn, firewall_id, rule_id):
    """
    Removes a rule from a firewall policy.
    """
    try:
        if module.check_mode:
            rule = oneandone_conn.get_firewall_policy_rule(
                firewall_id=firewall_id,
                rule_id=rule_id)
            if rule:
                return True
            return False

        firewall_policy = oneandone_conn.remove_firewall_rule(
            firewall_id=firewall_id,
            rule_id=rule_id
        )
        return firewall_policy
    except Exception as e:
        module.fail_json(msg=str(e))


def update_firewall_policy(module, oneandone_conn):
    """
    Updates a firewall policy based on input arguments.
    Firewall rules and server ips can be added/removed to/from
    firewall policy. Firewall policy name and description can be
    updated as well.

    module : AnsibleModule object
    oneandone_conn: authenticated oneandone object
    """
    try:
        firewall_policy_id = module.params.get('firewall_policy')
        name = module.params.get('name')
        description = module.params.get('description')
        add_server_ips = module.params.get('add_server_ips')
        remove_server_ips = module.params.get('remove_server_ips')
        add_rules = module.params.get('add_rules')
        remove_rules = module.params.get('remove_rules')

        changed = False

        firewall_policy = get_firewall_policy(oneandone_conn, firewall_policy_id, True)
        if firewall_policy is None:
            _check_mode(module, False)

        if name or description:
            _check_mode(module, True)
            firewall_policy = oneandone_conn.modify_firewall(
                firewall_id=firewall_policy['id'],
                name=name,
                description=description)
            changed = True

        if add_server_ips:
            if module.check_mode:
                _check_mode(module, _add_server_ips(module,
                                                    oneandone_conn,
                                                    firewall_policy['id'],
                                                    add_server_ips))

            firewall_policy = _add_server_ips(module, oneandone_conn, firewall_policy['id'], add_server_ips)
            changed = True

        if remove_server_ips:
            chk_changed = False
            for server_ip_id in remove_server_ips:
                if module.check_mode:
                    chk_changed |= _remove_firewall_server(module,
                                                           oneandone_conn,
                                                           firewall_policy['id'],
                                                           server_ip_id)

                _remove_firewall_server(module,
                                        oneandone_conn,
                                        firewall_policy['id'],
                                        server_ip_id)
            _check_mode(module, chk_changed)
            firewall_policy = get_firewall_policy(oneandone_conn, firewall_policy['id'], True)
            changed = True

        if add_rules:
            firewall_policy = _add_firewall_rules(module,
                                                  oneandone_conn,
                                                  firewall_policy['id'],
                                                  add_rules)
            _check_mode(module, firewall_policy)
            changed = True

        if remove_rules:
            chk_changed = False
            for rule_id in remove_rules:
                if module.check_mode:
                    chk_changed |= _remove_firewall_rule(module,
                                                         oneandone_conn,
                                                         firewall_policy['id'],
                                                         rule_id)

                _remove_firewall_rule(module,
                                      oneandone_conn,
                                      firewall_policy['id'],
                                      rule_id)
            _check_mode(module, chk_changed)
            firewall_policy = get_firewall_policy(oneandone_conn, firewall_policy['id'], True)
            changed = True

        return (changed, firewall_policy)
    except Exception as e:
        module.fail_json(msg=str(e))


def create_firewall_policy(module, oneandone_conn):
    """
    Create a new firewall policy.

    module : AnsibleModule object
    oneandone_conn: authenticated oneandone object
    """
    try:
        name = module.params.get('name')
        description = module.params.get('description')
        rules = module.params.get('rules')
        wait = module.params.get('wait')
        wait_timeout = module.params.get('wait_timeout')
        wait_interval = module.params.get('wait_interval')

        firewall_rules = []

        for rule in rules:
            firewall_rule = oneandone.client.FirewallPolicyRule(
                protocol=rule['protocol'],
                port_from=rule['port_from'],
                port_to=rule['port_to'],
                source=rule['source'])
            firewall_rules.append(firewall_rule)

        firewall_policy_obj = oneandone.client.FirewallPolicy(
            name=name,
            description=description
        )

        _check_mode(module, True)
        firewall_policy = oneandone_conn.create_firewall_policy(
            firewall_policy=firewall_policy_obj,
            firewall_policy_rules=firewall_rules
        )

        if wait:
            wait_for_resource_creation_completion(
                oneandone_conn,
                OneAndOneResources.firewall_policy,
                firewall_policy['id'],
                wait_timeout,
                wait_interval)

        firewall_policy = get_firewall_policy(oneandone_conn, firewall_policy['id'], True)  # refresh
        changed = True if firewall_policy else False

        _check_mode(module, False)

        return (changed, firewall_policy)
    except Exception as e:
        module.fail_json(msg=str(e))


def remove_firewall_policy(module, oneandone_conn):
    """
    Removes a firewall policy.

    module : AnsibleModule object
    oneandone_conn: authenticated oneandone object
    """
    try:
        fp_id = module.params.get('name')
        firewall_policy_id = get_firewall_policy(oneandone_conn, fp_id)
        if module.check_mode:
            if firewall_policy_id is None:
                _check_mode(module, False)
            _check_mode(module, True)
        firewall_policy = oneandone_conn.delete_firewall(firewall_policy_id)

        changed = True if firewall_policy else False

        return (changed, {
            'id': firewall_policy['id'],
            'name': firewall_policy['name']
        })
    except Exception as e:
        module.fail_json(msg=str(e))


def main():
    module = AnsibleModule(
        argument_spec=dict(
            auth_token=dict(
                type='str', no_log=True,
                default=os.environ.get('ONEANDONE_AUTH_TOKEN')),
            api_url=dict(
                type='str',
                default=os.environ.get('ONEANDONE_API_URL')),
            name=dict(type='str'),
            firewall_policy=dict(type='str'),
            description=dict(type='str'),
            rules=dict(type='list', elements="dict", default=[]),
            add_server_ips=dict(type='list', elements="str", default=[]),
            remove_server_ips=dict(type='list', elements="str", default=[]),
            add_rules=dict(type='list', elements="dict", default=[]),
            remove_rules=dict(type='list', elements="str", default=[]),
            wait=dict(type='bool', default=True),
            wait_timeout=dict(type='int', default=600),
            wait_interval=dict(type='int', default=5),
            state=dict(type='str', default='present', choices=['present', 'absent', 'update']),
        ),
        supports_check_mode=True
    )

    if not HAS_ONEANDONE_SDK:
        module.fail_json(msg='1and1 required for this module')

    if not module.params.get('auth_token'):
        module.fail_json(
            msg='The "auth_token" parameter or ' +
            'ONEANDONE_AUTH_TOKEN environment variable is required.')

    if not module.params.get('api_url'):
        oneandone_conn = oneandone.client.OneAndOneService(
            api_token=module.params.get('auth_token'))
    else:
        oneandone_conn = oneandone.client.OneAndOneService(
            api_token=module.params.get('auth_token'), api_url=module.params.get('api_url'))

    state = module.params.get('state')

    if state == 'absent':
        if not module.params.get('name'):
            module.fail_json(
                msg="'name' parameter is required to delete a firewall policy.")
        try:
            (changed, firewall_policy) = remove_firewall_policy(module, oneandone_conn)
        except Exception as e:
            module.fail_json(msg=str(e))

    elif state == 'update':
        if not module.params.get('firewall_policy'):
            module.fail_json(
                msg="'firewall_policy' parameter is required to update a firewall policy.")
        try:
            (changed, firewall_policy) = update_firewall_policy(module, oneandone_conn)
        except Exception as e:
            module.fail_json(msg=str(e))

    elif state == 'present':
        for param in ('name', 'rules'):
            if not module.params.get(param):
                module.fail_json(
                    msg="%s parameter is required for new firewall policies." % param)
        try:
            (changed, firewall_policy) = create_firewall_policy(module, oneandone_conn)
        except Exception as e:
            module.fail_json(msg=str(e))

    module.exit_json(changed=changed, firewall_policy=firewall_policy)


if __name__ == '__main__':
    main()
