# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
This file contains the REST API and CloudWatch entry-points for the MSAM backend.
"""

import os

import boto3
from chalice import Chalice, Rate

from chalicelib import cache
import chalicelib.channels as channel_tiles
import chalicelib.cloudwatch as cloudwatch_data
import chalicelib.layout as node_layout
import chalicelib.periodic as periodic_handlers
import chalicelib.settings as msam_settings
import chalicelib.notes as resource_notes

app = Chalice(app_name='msam')

# update one region at this interval
NODE_UPDATE_RATE_MINUTES = 5

# update one region at this interval
SSM_NODE_UPDATE_RATE_MINUTES = 5

# update connections at this interval
CONNECTION_UPDATE_RATE_MINUTES = 5

# update MSAM visuals from tags at this interval
TAG_UPDATE_RATE_MINUTES = 5

# update managed instance status and metrics at this interval
SSM_RUN_COMMAND_RATE_MINUTES = 1

# metrics generation interval
METRICS_GENERATION_RATE_HOURS = 1

# metrics reporting interval
METRICS_REPORTING_RATE_HOURS = 24

# table names generated by CloudFormation
ALARMS_TABLE_NAME = os.environ["ALARMS_TABLE_NAME"]
CHANNELS_TABLE_NAME = os.environ["CHANNELS_TABLE_NAME"]
CONTENT_TABLE_NAME = os.environ["CONTENT_TABLE_NAME"]
EVENTS_TABLE_NAME = os.environ["EVENTS_TABLE_NAME"]
LAYOUT_TABLE_NAME = os.environ["LAYOUT_TABLE_NAME"]
SETTINGS_TABLE_NAME = os.environ["SETTINGS_TABLE_NAME"]

# TTL provided via CloudFormation
CACHE_ITEM_TTL = int(os.environ["CACHE_ITEM_TTL"])

# stack name for CloudWatch namespaces
STACKNAME = os.environ.get("STACKNAME", "")

# DynamoDB
DYNAMO_CLIENT = boto3.client("dynamodb")
DYNAMO_RESOURCE = boto3.resource("dynamodb")

SSM_EVENT_PATTERN = {
    "source": ["aws.ssm"],
    "detail-type": ["EC2 Command Invocation Status-change Notification"],
    "detail": {
        "status": ["Success", "Failed", "TimedOut"]
    }
}


@app.route('/layout/view/{view}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_view_layout(view):
    """
    API entry point for retrieving all item positions in a view.
    """
    return node_layout.get_view_layout(view)


@app.route('/layout/nodes/{view}/{node_id}',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_view_layout(view, node_id):
    """
    API entry point for removing nodes from a view.
    """
    return node_layout.delete_node_layout(view, node_id)


@app.route(
    '/layout/nodes',
    cors=True,
    api_key_required=True,
    methods=['PUT', 'POST'],
    content_types=['application/json', 'application/x-www-form-urlencoded'])
def set_view_layout():
    """
    API entry point for setting nodes in a view. This adds new nodes and overwrites existing nodes. It does not replace the entire set.
    """
    return node_layout.set_node_layout(app.current_request.json_body)


@app.route('/layout/views',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_layout_views():
    """
    API entry point to delete all views (diagrams).
    """
    return node_layout.remove_all_diagrams()


@app.route('/channels', cors=True, api_key_required=True, methods=['GET'])
def get_channel_list():
    """
    API entry point to return all the current channel names.
    """
    return channel_tiles.get_channel_list()


@app.route('/channels', cors=True, api_key_required=True, methods=['DELETE'])
def delete_all_channels():
    """
    API entry point to delete all tiles.
    """
    return channel_tiles.delete_all_channels()


@app.route(
    '/channel/{name}',
    cors=True,
    api_key_required=True,
    methods=['PUT', 'POST'],
    content_types=['application/json', 'application/x-www-form-urlencoded'])
def set_channel_nodes(name):
    """
     API entry point to set the nodes for a given channel name.
    """
    return channel_tiles.set_channel_nodes(name, app.current_request.json_body)


@app.route('/channel/{name}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_channel_nodes(name):
    """
    API entry point to get the nodes for a given channel name.
    """
    return channel_tiles.get_channel_nodes(name)


@app.route('/channel/{name}',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_channel_nodes(name):
    """
    API entry point to delete a channel.
    """
    return channel_tiles.delete_channel_nodes(name)


@app.route(
    '/settings/{item_key}',
    cors=True,
    api_key_required=True,
    methods=['GET', 'PUT', 'POST', "DELETE"],
    content_types=['application/json', 'application/x-www-form-urlencoded'])
def application_settings(item_key):
    """
    API entry point to get or set the object value for a setting.
    """
    return msam_settings.application_settings(app.current_request, item_key)


@app.route('/cached/{service}/{region}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def cached_by_service_region(service, region):
    """
    API entry point to retrieve items from the cache under the service and region name.
    """
    return cache.cached_by_service_region(service, region)


@app.route('/cached/{service}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def cached_by_service(service):
    """
    API entry point to retrieve items from the cache under the service.
    """
    return cache.cached_by_service(service)


@app.route('/cached/arn/{arn}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def cached_by_arn(arn):
    """
    API entry point to retrieve items from the cache by arn.
    """
    return cache.cached_by_arn(arn)


@app.route(
    '/cached',
    cors=True,
    api_key_required=True,
    methods=['PUT', 'POST'],
    content_types=['application/json', 'application/x-www-form-urlencoded'])
def put_cached_data():
    """
    API entry point to add items to the cache.
    """
    return cache.put_cached_data(app.current_request)


@app.route('/cached/arn/{arn}',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_cached_data(arn):
    """
    API entry point to delete items from the cache.
    """
    return cache.delete_cached_data(arn)


@app.route('/regions', cors=True, api_key_required=True, methods=['GET'])
def regions():
    """
    API entry point to retrieve all regions based on EC2.
    """
    return cache.regions()


@app.route('/cloudwatch/alarms/all/{region}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_alarms_region(region):
    """
    API entry point to retrieve all CloudWatch alarms for a given region.
    """
    return cloudwatch_data.get_cloudwatch_alarms_region(region)


@app.lambda_function()
def incoming_cloudwatch_alarm(event, _):
    """
    Standard AWS Lambda entry point for receiving CloudWatch alarm notifications.
    """
    return cloudwatch_data.incoming_cloudwatch_alarm(event, _)


@app.route('/cloudwatch/alarm/{alarm_name}/region/{region}/subscribe',
           cors=True,
           api_key_required=True,
           methods=['PUT', 'POST'])
def subscribe_resource_to_alarm(alarm_name, region):
    """
    API entry point to subscribe one or more nodes to a CloudWatch alarm in a region.
    """
    return cloudwatch_data.subscribe_resource_to_alarm(app.current_request,
                                                       alarm_name, region)


@app.route('/cloudwatch/alarm/{alarm_name}/region/{region}/unsubscribe',
           cors=True,
           api_key_required=True,
           methods=['PUT', 'POST'])
def unsubscribe_resource_from_alarm(alarm_name, region):
    """
    API entry point to unsubscribe one or more nodes to a CloudWatch alarm in a region.
    """
    return cloudwatch_data.unsubscribe_resource_from_alarm(
        app.current_request, alarm_name, region)


@app.route('/cloudwatch/alarm/{alarm_name}/region/{region}/subscribers',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def subscribers_to_alarm(alarm_name, region):
    """
    API entry point to return subscribed nodes of a CloudWatch alarm in a region.
    """
    return cloudwatch_data.subscribers_to_alarm(alarm_name, region)


@app.route('/cloudwatch/alarms/{alarm_state}/subscribers',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def subscribed_with_state(alarm_state):
    """
    API entry point to return nodes subscribed to alarms in a given alarm state (OK, ALARM, INSUFFICIENT_DATA).
    """
    return cloudwatch_data.subscribed_with_state(alarm_state)


@app.route('/cloudwatch/alarms/subscriber/{resource_arn}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def alarms_for_subscriber(resource_arn):
    """
    API entry point to return all alarms subscribed to by a node.
    """
    return cloudwatch_data.alarms_for_subscriber(resource_arn)


@app.route('/cloudwatch/alarms/subscribed',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def all_subscribed_alarms():
    """
    API entry point to return a unique list of all subscribed alarms in the database.
    """
    return cloudwatch_data.all_subscribed_alarms()


@app.route('/cloudwatch/alarms/subscribed',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_alarm_subscriptions():
    """
    API entry point to delete all alarm subscriptions.
    """
    return cloudwatch_data.delete_all_subscriptions()


@app.route('/cloudwatch/events/state/{state}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_events_state(state):
    """
    API entry point to retrieve all alert events in a given state (set, clear).
    """
    return cloudwatch_data.get_cloudwatch_events_state(state)


@app.route('/cloudwatch/events/state/{state}/{source}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_events_state_source(state, source):
    """
    API entry point to retrieve all alert events in a given state (set, clear) from a specific source.
    """
    return cloudwatch_data.get_cloudwatch_events_state_source(state, source)


@app.route('/cloudwatch/events/all/{resource_arn}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_events_resource_arn(resource_arn):
    """
    API entry point to return all CloudWatch events related to a node.
    """
    limit = 100
    if app.current_request.query_params is not None and app.current_request.query_params.get(
            'limit') == 'true':
        limit = app.current_request.query_params.get('limit')
    return cloudwatch_data.get_cloudwatch_events_resource(
        resource_arn=resource_arn, limit=limit)


@app.route('/cloudwatch/events/{resource_arn}/{start_time}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_events_resource_arn_start(resource_arn, start_time):
    """
    API entry point to return all CloudWatch events related to a node from start_time to now.
    """
    limit = 100
    if app.current_request.query_params is not None and app.current_request.query_params.get('limit') == 'true':
        limit = app.current_request.query_params.get('limit')
    return cloudwatch_data.get_cloudwatch_events_resource(
        resource_arn=resource_arn, start_time=start_time, limit=limit)


@app.route('/cloudwatch/events/{resource_arn}/{start_time}/{end_time}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_cloudwatch_events_resource_arn_start_end(resource_arn, start_time,
                                                 end_time):
    """
    API entry point to return all CloudWatch events related to a node for a given time range.
    """
    limit = 100
    if app.current_request.query_params is not None and app.current_request.query_params.get('limit') == 'true':
        limit = app.current_request.query_params.get('limit')
    return cloudwatch_data.get_cloudwatch_events_resource(
        resource_arn, start_time, end_time, limit)


@app.route('/ping', cors=True, api_key_required=True, methods=['GET'])
def ping():
    """
    API entry point to test the API key authentication and retrieve the build timestamp.
    """
    return {
        "message": "pong",
        "buildstamp": os.environ["BUILD_STAMP"],
        "version": os.environ["VERSION"]
    }


@app.schedule(Rate(NODE_UPDATE_RATE_MINUTES, unit=Rate.MINUTES))
def update_nodes(_):
    """
    Entry point for the CloudWatch scheduled task to discover and cache services.
    """
    return periodic_handlers.update_nodes()


@app.schedule(Rate(CONNECTION_UPDATE_RATE_MINUTES, unit=Rate.MINUTES))
def update_connections(_):
    """
    Entry point for the CloudWatch scheduled task to discover and cache services.
    """
    return periodic_handlers.update_connections()


@app.schedule(Rate(TAG_UPDATE_RATE_MINUTES, unit=Rate.MINUTES))
def update_from_tags(_):
    """
    Entry point for the CloudWatch scheduled task to discover and cache services.
    """
    return periodic_handlers.update_from_tags()


@app.schedule(Rate(SSM_RUN_COMMAND_RATE_MINUTES, unit=Rate.MINUTES))
def ssm_run_command(_):
    """
    Entry point for the CloudWatch scheduled task to check status of managed instances.
    """
    return periodic_handlers.ssm_run_command()


@app.on_cw_event(SSM_EVENT_PATTERN)
def process_ssm_run_command(event):
    """
    Lambda for handling  task to check status of managed instances.
    """
    return periodic_handlers.process_ssm_run_command(event)


@app.schedule(Rate(SSM_NODE_UPDATE_RATE_MINUTES, unit=Rate.MINUTES))
def update_ssm_nodes(_):
    """
    Entry point for the CloudWatch scheduled task to check status of managed instances.
    """
    return periodic_handlers.update_ssm_nodes()


@app.schedule(Rate(METRICS_GENERATION_RATE_HOURS, unit=Rate.HOURS))
def generate_metrics(_):
    """
    Entry point for the CloudWatch scheduled task to generate resource metrics.
    """
    return periodic_handlers.generate_metrics(STACKNAME)


@app.schedule(Rate(METRICS_REPORTING_RATE_HOURS, unit=Rate.HOURS))
def report_metrics(_):
    """
    Entry point for the CloudWatch scheduled task to report anonymous resource metrics.
    """
    return periodic_handlers.report_metrics(STACKNAME, METRICS_REPORTING_RATE_HOURS)


@app.route('/notes/{resource_arn}',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def get_resource_notes(resource_arn):
    """
    API entry point to return notes for a given resource.
    """
    return resource_notes.get_resource_notes(resource_arn)


@app.route('/notes',
           cors=True,
           api_key_required=True,
           methods=['GET'])
def all_notes():
    """
    API entry point to return notes for all resource.
    """
    return resource_notes.get_all_notes()


@app.route('/notes/{resource_arn}',
           cors=True,
           api_key_required=True,
           methods=['POST'],
           content_types=['text/plain', 'application/json'])
def update_resource_notes(resource_arn):
    """
    API entry point to update notes of a given resource.
    """
    return resource_notes.update_resource_notes(resource_arn, app.current_request)

@app.route('/notes/{resource_arn}',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_resource_notes(resource_arn):
    """
    API entry point to return notes for a given resource.
    """
    return resource_notes.delete_resource_notes(resource_arn)

@app.route('/notes',
           cors=True,
           api_key_required=True,
           methods=['DELETE'])
def delete_all_notes():
    """
    API entry point to return notes for a given resource.
    """
    return resource_notes.delete_all_notes_proxy()

@app.lambda_function(name='DeleteAllResourceNotes')
def delete_all_resource_notes(event, context):
    """
    Function that does actual deletion of all resource notes.
    """
    return resource_notes.delete_all_notes()