#!/usr/bin/python
#
# Scaleway Load-balancer management module
#
# Copyright (C) 2018 Online SAS.
# https://www.scaleway.com
#
# 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 annotations


DOCUMENTATION = r"""
module: scaleway_lb
short_description: Scaleway load-balancer management module
author: Remy Leone (@remyleone)
description:
  - This module manages load-balancers on Scaleway.
extends_documentation_fragment:
  - community.general.scaleway
  - community.general.attributes
  - community.general.scaleway.actiongroup_scaleway

attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
  action_group:
    version_added: 11.3.0

options:

  name:
    type: str
    description:
      - Name of the load-balancer.
    required: true

  description:
    type: str
    description:
      - Description of the load-balancer.
    required: true

  organization_id:
    type: str
    description:
      - Organization identifier.
    required: true

  state:
    type: str
    description:
      - Indicate desired state of the instance.
    default: present
    choices:
      - present
      - absent

  region:
    type: str
    description:
      - Scaleway zone.
    required: true
    choices:
      - nl-ams
      - fr-par
      - pl-waw

  tags:
    type: list
    elements: str
    default: []
    description:
      - List of tags to apply to the load-balancer.
  wait:
    description:
      - Wait for the load-balancer to reach its desired state before returning.
    type: bool
    default: false

  wait_timeout:
    type: int
    description:
      - Time to wait for the load-balancer to reach the expected state.
    default: 300

  wait_sleep_time:
    type: int
    description:
      - Time to wait before every attempt to check the state of the load-balancer.
    default: 3
"""

EXAMPLES = r"""
- name: Create a load-balancer
  community.general.scaleway_lb:
    name: foobar
    state: present
    organization_id: 951df375-e094-4d26-97c1-ba548eeb9c42
    region: fr-par
    tags:
      - hello

- name: Delete a load-balancer
  community.general.scaleway_lb:
    name: foobar
    state: absent
    organization_id: 951df375-e094-4d26-97c1-ba548eeb9c42
    region: fr-par
"""

RETURN = r"""
scaleway_lb:
  description: The load-balancer object.
  returned: success
  type: dict
  sample:
    {
      "backend_count": 0,
      "frontend_count": 0,
      "description": "Description of my load-balancer",
      "id": "00000000-0000-0000-0000-000000000000",
      "instances": [
        {
          "id": "00000000-0000-0000-0000-000000000000",
          "ip_address": "10.0.0.1",
          "region": "fr-par",
          "status": "ready"
        },
        {
          "id": "00000000-0000-0000-0000-000000000000",
          "ip_address": "10.0.0.2",
          "region": "fr-par",
          "status": "ready"
        }
      ],
      "ip": [
        {
          "id": "00000000-0000-0000-0000-000000000000",
          "ip_address": "192.168.0.1",
          "lb_id": "00000000-0000-0000-0000-000000000000",
          "region": "fr-par",
          "organization_id": "00000000-0000-0000-0000-000000000000",
          "reverse": ""
        }
      ],
      "name": "lb_ansible_test",
      "organization_id": "00000000-0000-0000-0000-000000000000",
      "region": "fr-par",
      "status": "ready",
      "tags": [
        "first_tag",
        "second_tag"
      ]
    }
"""

import datetime
import time
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.datetime import now
from ansible_collections.community.general.plugins.module_utils.scaleway import (
    SCALEWAY_REGIONS,
    SCALEWAY_ENDPOINT,
    scaleway_argument_spec,
    Scaleway,
)

STABLE_STATES = ("ready", "absent")

MUTABLE_ATTRIBUTES = ("name", "description")


def payload_from_wished_lb(wished_lb):
    return {
        "organization_id": wished_lb["organization_id"],
        "name": wished_lb["name"],
        "tags": wished_lb["tags"],
        "description": wished_lb["description"],
    }


def fetch_state(api, lb):
    api.module.debug(f"fetch_state of load-balancer: {lb['id']}")
    response = api.get(path=f"{api.api_path}/{lb['id']}")

    if response.status_code == 404:
        return "absent"

    if not response.ok:
        msg = f"Error during state fetching: ({response.status_code}) {response.json}"
        api.module.fail_json(msg=msg)

    try:
        api.module.debug(f"Load-balancer {lb['id']} in state: {response.json['status']}")
        return response.json["status"]
    except KeyError:
        api.module.fail_json(msg=f"Could not fetch state in {response.json}")


def wait_to_complete_state_transition(api, lb, force_wait=False):
    wait = api.module.params["wait"]
    if not (wait or force_wait):
        return
    wait_timeout = api.module.params["wait_timeout"]
    wait_sleep_time = api.module.params["wait_sleep_time"]

    start = now()
    end = start + datetime.timedelta(seconds=wait_timeout)
    while now() < end:
        api.module.debug("We are going to wait for the load-balancer to finish its transition")
        state = fetch_state(api, lb)
        if state in STABLE_STATES:
            api.module.debug("It seems that the load-balancer is not in transition anymore.")
            api.module.debug(f"load-balancer in state: {fetch_state(api, lb)}")
            break
        time.sleep(wait_sleep_time)
    else:
        api.module.fail_json(msg="Server takes too long to finish its transition")


def lb_attributes_should_be_changed(target_lb, wished_lb):
    diff = {attr: wished_lb[attr] for attr in MUTABLE_ATTRIBUTES if target_lb[attr] != wished_lb[attr]}

    if diff:
        return {attr: wished_lb[attr] for attr in MUTABLE_ATTRIBUTES}

    return {}


def present_strategy(api, wished_lb):
    changed = False

    response = api.get(path=api.api_path)
    if not response.ok:
        api.module.fail_json(msg=f"Error getting load-balancers [{response.status_code}: {response.json['message']}]")

    lbs_list = response.json["lbs"]
    lb_lookup = {lb["name"]: lb for lb in lbs_list}

    if wished_lb["name"] not in lb_lookup.keys():
        changed = True
        if api.module.check_mode:
            return changed, {"status": "A load-balancer would be created."}

        # Create Load-balancer
        api.warn(payload_from_wished_lb(wished_lb))
        creation_response = api.post(path=api.api_path, data=payload_from_wished_lb(wished_lb))

        if not creation_response.ok:
            msg = f"Error during lb creation: {creation_response.info['msg']}: '{creation_response.json['message']}' ({creation_response.json})"
            api.module.fail_json(msg=msg)

        wait_to_complete_state_transition(api=api, lb=creation_response.json)
        response = api.get(path=f"{api.api_path}/{creation_response.json['id']}")
        return changed, response.json

    target_lb = lb_lookup[wished_lb["name"]]
    patch_payload = lb_attributes_should_be_changed(target_lb=target_lb, wished_lb=wished_lb)

    if not patch_payload:
        return changed, target_lb

    changed = True
    if api.module.check_mode:
        return changed, {"status": "Load-balancer attributes would be changed."}

    lb_patch_response = api.put(path=f"{api.api_path}/{target_lb['id']}", data=patch_payload)

    if not lb_patch_response.ok:
        api.module.fail_json(
            msg=f"Error during load-balancer attributes update: [{lb_patch_response.status_code}: {lb_patch_response.json['message']}]"
        )

    wait_to_complete_state_transition(api=api, lb=target_lb)
    return changed, lb_patch_response.json


def absent_strategy(api, wished_lb):
    response = api.get(path=api.api_path)
    changed = False

    status_code = response.status_code
    lbs_json = response.json
    lbs_list = lbs_json["lbs"]

    if not response.ok:
        api.module.fail_json(msg=f"Error getting load-balancers [{status_code}: {response.json['message']}]")

    lb_lookup = {lb["name"]: lb for lb in lbs_list}
    if wished_lb["name"] not in lb_lookup.keys():
        return changed, {}

    target_lb = lb_lookup[wished_lb["name"]]
    changed = True
    if api.module.check_mode:
        return changed, {"status": "Load-balancer would be destroyed"}

    wait_to_complete_state_transition(api=api, lb=target_lb, force_wait=True)
    response = api.delete(path=f"{api.api_path}/{target_lb['id']}")
    if not response.ok:
        api.module.fail_json(msg=f"Error deleting load-balancer [{response.status_code}: {response.json}]")

    wait_to_complete_state_transition(api=api, lb=target_lb)
    return changed, response.json


state_strategy = {"present": present_strategy, "absent": absent_strategy}


def core(module):
    region = module.params["region"]
    wished_load_balancer = {
        "state": module.params["state"],
        "name": module.params["name"],
        "description": module.params["description"],
        "tags": module.params["tags"],
        "organization_id": module.params["organization_id"],
    }
    module.params["api_url"] = SCALEWAY_ENDPOINT
    api = Scaleway(module=module)
    api.api_path = f"lb/v1/regions/{region}/lbs"

    changed, summary = state_strategy[wished_load_balancer["state"]](api=api, wished_lb=wished_load_balancer)
    module.exit_json(changed=changed, scaleway_lb=summary)


def main():
    argument_spec = scaleway_argument_spec()
    argument_spec.update(
        dict(
            name=dict(required=True),
            description=dict(required=True),
            region=dict(required=True, choices=SCALEWAY_REGIONS),
            state=dict(choices=list(state_strategy.keys()), default="present"),
            tags=dict(type="list", elements="str", default=[]),
            organization_id=dict(required=True),
            wait=dict(type="bool", default=False),
            wait_timeout=dict(type="int", default=300),
            wait_sleep_time=dict(type="int", default=3),
        )
    )
    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
    )

    core(module)


if __name__ == "__main__":
    main()
