package com.airbnb.billow;

import com.amazonaws.services.elasticache.model.DescribeReplicationGroupsRequest;
import com.amazonaws.services.elasticache.model.DescribeReplicationGroupsResult;
import com.amazonaws.services.elasticache.model.NodeGroup;
import com.amazonaws.services.elasticache.model.NodeGroupMember;
import com.amazonaws.services.elasticache.model.ReplicationGroup;
import com.amazonaws.services.elasticsearch.model.DescribeElasticsearchDomainRequest;
import com.amazonaws.services.elasticsearch.model.DescribeElasticsearchDomainResult;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableCollection;
import com.amazonaws.services.dynamodbv2.model.ListTablesResult;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.SecurityGroup;
import com.amazonaws.services.elasticache.AmazonElastiCacheClient;
import com.amazonaws.services.elasticache.model.CacheCluster;
import com.amazonaws.services.elasticache.model.DescribeCacheClustersRequest;
import com.amazonaws.services.elasticache.model.DescribeCacheClustersResult;
import com.amazonaws.services.elasticsearch.AWSElasticsearchClient;
import com.amazonaws.services.elasticsearch.model.DomainInfo;
import com.amazonaws.services.elasticsearch.model.ListDomainNamesRequest;
import com.amazonaws.services.elasticsearch.model.ListDomainNamesResult;
import com.amazonaws.services.elasticsearch.model.ListTagsRequest;
import com.amazonaws.services.elasticsearch.model.ListTagsResult;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagement;
import com.amazonaws.services.identitymanagement.model.AccessKeyMetadata;
import com.amazonaws.services.identitymanagement.model.ListAccessKeysRequest;
import com.amazonaws.services.identitymanagement.model.ListUsersRequest;
import com.amazonaws.services.identitymanagement.model.ListUsersResult;
import com.amazonaws.services.identitymanagement.model.User;
import com.amazonaws.services.rds.AmazonRDSClient;
import com.amazonaws.services.rds.model.DBCluster;
import com.amazonaws.services.rds.model.DBClusterMember;
import com.amazonaws.services.rds.model.DBClusterSnapshot;
import com.amazonaws.services.rds.model.DBInstance;
import com.amazonaws.services.rds.model.DBSnapshot;
import com.amazonaws.services.rds.model.DescribeDBClusterSnapshotsRequest;
import com.amazonaws.services.rds.model.DescribeDBClusterSnapshotsResult;
import com.amazonaws.services.rds.model.DescribeDBClustersRequest;
import com.amazonaws.services.rds.model.DescribeDBClustersResult;
import com.amazonaws.services.rds.model.DescribeDBInstancesRequest;
import com.amazonaws.services.rds.model.DescribeDBInstancesResult;
import com.amazonaws.services.rds.model.DescribeDBSnapshotsRequest;
import com.amazonaws.services.rds.model.DescribeDBSnapshotsResult;
import com.amazonaws.services.rds.model.ListTagsForResourceRequest;
import com.amazonaws.services.rds.model.ListTagsForResourceResult;
import com.amazonaws.services.sqs.AmazonSQSClient;
import com.amazonaws.services.sqs.model.ListQueuesResult;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;

@Slf4j
@Data
public class AWSDatabase {
    private final ImmutableMultimap<String, EC2Instance> ec2Instances;
    private final ImmutableMultimap<String, DynamoTable> dynamoTables;
    private final ImmutableMultimap<String, RDSInstance> rdsInstances;
    private final ImmutableMultimap<String, SecurityGroup> ec2SGs;
    private final ImmutableMultimap<String, SQSQueue> sqsQueues;
    private final ImmutableMultimap<String, ElasticacheCluster> elasticacheClusters;
    private final ImmutableMultimap<String, ElasticsearchCluster> elasticsearchClusters;
    private final ImmutableList<IAMUserWithKeys> iamUsers;
    private final long timestamp;
    private String awsAccountNumber;
    private String awsARNPartition;

    AWSDatabase(final Map<String, AmazonEC2Client> ec2Clients,
                final Map<String, AmazonRDSClient> rdsClients,
                final Map<String, AmazonDynamoDBClient> dynamoClients,
                final Map<String, AmazonSQSClient> sqsClients,
                final Map<String, AmazonElastiCacheClient> elasticacheClients,
                final Map<String, AWSElasticsearchClient> elasticsearchClients,
                final AmazonIdentityManagement iamClient,
                final String configAWSAccountNumber,
                final String configAWSARNPartition) {
        timestamp = System.currentTimeMillis();
        log.info("Building AWS DB with timestamp {}", timestamp);

        log.info("Getting EC2 instances");
        final ImmutableMultimap.Builder<String, EC2Instance> ec2InstanceBuilder = new ImmutableMultimap.Builder<>();
        final ImmutableMultimap.Builder<String, DynamoTable> dynamoTableBuilder = new ImmutableMultimap.Builder<>();
        final ImmutableMultimap.Builder<String, SQSQueue> sqsQueueBuilder = new ImmutableMultimap.Builder<>();
        final ImmutableMultimap.Builder<String, ElasticacheCluster> elasticacheClusterBuilder =
            new ImmutableMultimap.Builder<>();
        final ImmutableMultimap.Builder<String, ElasticsearchCluster> elasticsearchClusterBuilder =
            new ImmutableMultimap.Builder<>();

        if (configAWSAccountNumber == null) {
            awsAccountNumber = "";
        } else {
            log.info("using account number '{}' from config", configAWSAccountNumber);
            awsAccountNumber = configAWSAccountNumber;
        }

        if (configAWSARNPartition == null) {
            awsARNPartition = "aws";
        } else {
            log.info("using arn partition '{}' from config", configAWSARNPartition);
            awsARNPartition = configAWSARNPartition;
        }

        /*
         * IAM keys
         * Put this in the beginning to populate the awsAccountNumber.
         */

        log.info("Getting IAM keys");
        final ImmutableList.Builder<IAMUserWithKeys> usersBuilder = new ImmutableList.Builder<>();

        final ListUsersRequest listUsersRequest = new ListUsersRequest();
        ListUsersResult listUsersResult;
        do {
            log.debug("Performing IAM request: {}", listUsersRequest);
            listUsersResult = iamClient.listUsers(listUsersRequest);
            final List<User> users = listUsersResult.getUsers();
            log.debug("Found {} users", users.size());
            for (User user : users) {
                final ListAccessKeysRequest listAccessKeysRequest = new ListAccessKeysRequest();
                listAccessKeysRequest.setUserName(user.getUserName());
                final List<AccessKeyMetadata> accessKeyMetadata = iamClient.listAccessKeys(listAccessKeysRequest).getAccessKeyMetadata();

                final IAMUserWithKeys userWithKeys = new IAMUserWithKeys(user, ImmutableList.<AccessKeyMetadata>copyOf(accessKeyMetadata));
                usersBuilder.add(userWithKeys);

                if (awsAccountNumber.isEmpty()) {
                    awsAccountNumber = user.getArn().split(":")[4];
                }
            }
            listUsersRequest.setMarker(listUsersResult.getMarker());
        } while (listUsersResult.isTruncated());
        this.iamUsers = usersBuilder.build();

        /*
         * ElasticCache
         */

        for (Map.Entry<String, AmazonElastiCacheClient> clientPair : elasticacheClients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonElastiCacheClient client = clientPair.getValue();
            final Map<String, NodeGroupMember> clusterIdToNodeGroupMember = new HashMap<>();
            DescribeCacheClustersRequest describeCacheClustersRequest = new DescribeCacheClustersRequest();
            DescribeReplicationGroupsRequest describeReplicationGroupsRequest = new DescribeReplicationGroupsRequest();
            DescribeCacheClustersResult describeCacheClustersResult;
            DescribeReplicationGroupsResult describeReplicationGroupsResult;

            do {
                log.info("Getting Elasticache replication groups from {} with marker {}", regionName, describeReplicationGroupsRequest.getMarker());

                describeReplicationGroupsResult = client.describeReplicationGroups(describeReplicationGroupsRequest);

                for (ReplicationGroup replicationGroup: describeReplicationGroupsResult.getReplicationGroups()) {
                    for (NodeGroup nodeGroup: replicationGroup.getNodeGroups()) {
                        for (NodeGroupMember nodeGroupMember: nodeGroup.getNodeGroupMembers()) {
                            clusterIdToNodeGroupMember.put(nodeGroupMember.getCacheClusterId(), nodeGroupMember);
                        }
                    }
                }

                describeReplicationGroupsRequest.setMarker(describeReplicationGroupsResult.getMarker());
            } while (describeReplicationGroupsResult.getMarker() != null);

            do {
                log.info("Getting Elasticache from {} with marker {}", regionName, describeCacheClustersRequest.getMarker());

                describeCacheClustersResult = client.describeCacheClusters(describeCacheClustersRequest);
                int cntClusters = 0;

                for (CacheCluster cluster : describeCacheClustersResult.getCacheClusters()) {
                    com.amazonaws.services.elasticache.model.ListTagsForResourceRequest tagsRequest =
                        new com.amazonaws.services.elasticache.model.ListTagsForResourceRequest()
                            .withResourceName(elasticacheARN(awsARNPartition, regionName, awsAccountNumber, cluster));

                    com.amazonaws.services.elasticache.model.ListTagsForResourceResult tagsResult =
                        client.listTagsForResource(tagsRequest);
                    elasticacheClusterBuilder.putAll(regionName, new ElasticacheCluster(cluster, clusterIdToNodeGroupMember.get(cluster.getCacheClusterId()), tagsResult.getTagList()));
                    cntClusters++;
                }

                log.debug("Found {} cache clusters in {}", cntClusters, regionName);

                describeCacheClustersRequest.setMarker(describeCacheClustersResult.getMarker());
            } while (describeCacheClustersResult.getMarker() != null);


        }
        this.elasticacheClusters = elasticacheClusterBuilder.build();

        /*
         * Elasticsearch
         */

        for (Map.Entry<String, AWSElasticsearchClient> clientPair : elasticsearchClients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AWSElasticsearchClient client = clientPair.getValue();
            ListDomainNamesRequest domainNamesRequest = new ListDomainNamesRequest();
            ListDomainNamesResult domainNamesResult = client.listDomainNames(domainNamesRequest);

            List<DomainInfo> domainInfoList = domainNamesResult.getDomainNames();
            for (DomainInfo domainInfo : domainInfoList) {
                ListTagsRequest listTagsRequest = new ListTagsRequest();
                listTagsRequest.setARN(elasticsearchARN(awsARNPartition, regionName, awsAccountNumber, domainInfo.getDomainName()));
                ListTagsResult tagList = client.listTags(listTagsRequest);

                DescribeElasticsearchDomainRequest describeDomainRequest = new DescribeElasticsearchDomainRequest();
                describeDomainRequest.setDomainName(domainInfo.getDomainName());
                DescribeElasticsearchDomainResult describeDomainResult = client.describeElasticsearchDomain(describeDomainRequest);

                elasticsearchClusterBuilder.putAll(regionName, new ElasticsearchCluster(describeDomainResult.getDomainStatus(), tagList.getTagList()));
            }
            log.debug("Found {} Elasticsearch domains in {}", domainInfoList.size(), regionName);

        }
        this.elasticsearchClusters = elasticsearchClusterBuilder.build();

        /*
         * SQS Queues
         */

        for (Map.Entry<String, AmazonSQSClient> clientPair : sqsClients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonSQSClient client = clientPair.getValue();
            ListQueuesResult queues = client.listQueues();

            log.info("Getting SQS from {}", regionName);
            int cnt = 0;
            for (String url : queues.getQueueUrls()) {
                List<String> attrs = new ArrayList<>();
                attrs.add("All");

                Map<String, String> map = client.getQueueAttributes(url, attrs).getAttributes();
                String approximateNumberOfMessagesDelayed = map.get(SQSQueue.ATTR_APPROXIMATE_NUMBER_OF_MESSAGES_DELAYED);
                String receiveMessageWaitTimeSeconds = map.get(SQSQueue.ATTR_RECEIVE_MESSAGE_WAIT_TIME_SECONDS);
                String createdTimestamp = map.get(SQSQueue.ATTR_CREATED_TIMESTAMP);
                String delaySeconds = map.get(SQSQueue.ATTR_DELAY_SECONDS);
                String messageRetentionPeriod = map.get(SQSQueue.ATTR_MESSAGE_RETENTION_PERIOD);
                String maximumMessageSize = map.get(SQSQueue.ATTR_MAXIMUM_MESSAGE_SIZE);
                String visibilityTimeout = map.get(SQSQueue.ATTR_VISIBILITY_TIMEOUT);
                String approximateNumberOfMessages = map.get(SQSQueue.ATTR_APPROXIMATE_NUMBER_OF_MESSAGES);
                String lastModifiedTimestamp = map.get(SQSQueue.ATTR_LAST_MODIFIED_TIMESTAMP);
                String queueArn = map.get(SQSQueue.ATTR_QUEUE_ARN);

                SQSQueue queue = new SQSQueue(url, Long.valueOf(approximateNumberOfMessagesDelayed),
                    Long.valueOf(receiveMessageWaitTimeSeconds), Long.valueOf(createdTimestamp),
                    Long.valueOf(delaySeconds), Long.valueOf(messageRetentionPeriod), Long.valueOf(maximumMessageSize),
                    Long.valueOf(visibilityTimeout), Long.valueOf(approximateNumberOfMessages),
                    Long.valueOf(lastModifiedTimestamp), queueArn);

                sqsQueueBuilder.putAll(regionName, queue);
                cnt++;
            }

            log.debug("Found {} queues in {}", cnt, regionName);
        }
        this.sqsQueues = sqsQueueBuilder.build();

        /*
         * DynamoDB Tables
         */

        for (Map.Entry<String, AmazonDynamoDBClient> clientPair : dynamoClients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonDynamoDBClient client = clientPair.getValue();
            final DynamoDB dynamoDB = new DynamoDB(client);
            TableCollection<ListTablesResult> tables = dynamoDB.listTables();
            Iterator<Table> iterator = tables.iterator();

            log.info("Getting DynamoDB from {}", regionName);
            int cnt = 0;
            while (iterator.hasNext()) {
                Table table = iterator.next();
                dynamoTableBuilder.putAll(regionName, new DynamoTable(table));
                cnt++;
            }

            log.debug("Found {} dynamodbs in {}", cnt, regionName);
        }
        this.dynamoTables = dynamoTableBuilder.build();

        /*
         * EC2 Instances
         */

        for (Map.Entry<String, AmazonEC2Client> clientPair : ec2Clients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonEC2Client client = clientPair.getValue();

            log.info("Getting EC2 reservations from {}", regionName);

            final List<Reservation> reservations = client.describeInstances().getReservations();
            log.debug("Found {} reservations in {}", reservations.size(), regionName);
            for (Reservation reservation : reservations) {
                for (Instance instance : reservation.getInstances())
                    ec2InstanceBuilder.putAll(regionName, new EC2Instance(instance));
            }
        }
        this.ec2Instances = ec2InstanceBuilder.build();

        /*
         * EC2 security groups
         */

        log.info("Getting EC2 security groups");
        final ImmutableMultimap.Builder<String, SecurityGroup> ec2SGbuilder = new ImmutableMultimap.Builder<String, SecurityGroup>();
        for (Map.Entry<String, AmazonEC2Client> clientPair : ec2Clients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonEC2Client client = clientPair.getValue();
            final List<SecurityGroup> securityGroups = client.describeSecurityGroups().getSecurityGroups();
            log.debug("Found {} security groups in {}", securityGroups.size(), regionName);
            ec2SGbuilder.putAll(regionName, securityGroups);
        }
        this.ec2SGs = ec2SGbuilder.build();


        /*
         * RDS Instances
         */

        log.info("Getting RDS instances and clusters");
        final ImmutableMultimap.Builder<String, RDSInstance> rdsBuilder = new ImmutableMultimap.Builder<String, RDSInstance>();

        for (Map.Entry<String, AmazonRDSClient> clientPair : rdsClients.entrySet()) {
            final String regionName = clientPair.getKey();
            final AmazonRDSClient client = clientPair.getValue();
            final Map<String, DBCluster> instanceIdToCluster = new HashMap<>();

            DescribeDBClustersRequest dbClustersRequest = new DescribeDBClustersRequest();
            DescribeDBClustersResult clustersResult;

            log.info("Getting RDS clusters from {}", regionName);

            do {
                log.debug("Performing RDS request: {}", dbClustersRequest);
                clustersResult = client.describeDBClusters(dbClustersRequest);
                final List<DBCluster> clusters = clustersResult.getDBClusters();
                log.debug("Found {} DB clusters", clusters.size());
                for (DBCluster cluster : clusters) {
                    for (DBClusterMember member : cluster.getDBClusterMembers()) {
                        instanceIdToCluster.put(member.getDBInstanceIdentifier(), cluster);
                    }
                }
                dbClustersRequest.setMarker(clustersResult.getMarker());
            } while (clustersResult.getMarker() != null);

            DescribeDBInstancesRequest rdsRequest = new DescribeDBInstancesRequest();
            DescribeDBInstancesResult result;

            log.info("Getting RDS instances from {}", regionName);

            do {
                log.debug("Performing RDS request: {}", rdsRequest);
                result = client.describeDBInstances(rdsRequest);
                final List<DBInstance> instances = result.getDBInstances();
                log.debug("Found {} RDS instances", instances.size());
                for (DBInstance instance : instances) {
                    ListTagsForResourceRequest tagsRequest = new ListTagsForResourceRequest()
                            .withResourceName(rdsARN(awsARNPartition, regionName, awsAccountNumber, instance));

                    ListTagsForResourceResult tagsResult = client.listTagsForResource(tagsRequest);

                    List<String> snapshots = new ArrayList<>();
                    // Get snapshot for masters only.
                    if (RDSInstance.checkIfMaster(instance, instanceIdToCluster.get(instance.getDBInstanceIdentifier()))) {
                       if ("aurora".equals(instance.getEngine()) || "aurora-mysql".equals(instance.getEngine())) {
                           DescribeDBClusterSnapshotsRequest snapshotsRequest = new DescribeDBClusterSnapshotsRequest()
                               .withDBClusterIdentifier(instance.getDBClusterIdentifier());
                           DescribeDBClusterSnapshotsResult snapshotsResult = client.describeDBClusterSnapshots(snapshotsRequest);
                           for (DBClusterSnapshot s : snapshotsResult.getDBClusterSnapshots()) {
                               snapshots.add(s.getDBClusterSnapshotIdentifier());
                           }
                       } else {
                           DescribeDBSnapshotsRequest snapshotsRequest = new DescribeDBSnapshotsRequest()
                               .withDBInstanceIdentifier(instance.getDBInstanceIdentifier());
                           DescribeDBSnapshotsResult snapshotsResult = client.describeDBSnapshots(snapshotsRequest);
                           for (DBSnapshot s : snapshotsResult.getDBSnapshots()) {
                               snapshots.add(s.getDBSnapshotIdentifier());
                           }
                       }
                    }
                    rdsBuilder.putAll(regionName, new RDSInstance(instance,
                        instanceIdToCluster.get(instance.getDBInstanceIdentifier()), tagsResult.getTagList(), snapshots));

                }
                rdsRequest.setMarker(result.getMarker());
            } while (result.getMarker() != null);
        }
        this.rdsInstances = rdsBuilder.build();

        log.info("Done building AWS DB");
    }

    public long getAgeInMs() {
        return System.currentTimeMillis() - getTimestamp();
    }

    private String rdsARN(String partition, String regionName, String accountNumber, DBInstance instance) {
        return String.format(
                "arn:%s:rds:%s:%s:db:%s",
                partition,
                regionName,
                accountNumber,
                instance.getDBInstanceIdentifier()
        );
    }

    private String elasticacheARN(String partition, String regionName, String accountNumber, CacheCluster cacheCluster) {
        return String.format(
                "arn:%s:elasticache:%s:%s:cluster:%s",
                partition,
                regionName,
                accountNumber,
                cacheCluster.getCacheClusterId()
        );
    }

    private String elasticsearchARN(String partition, String regionName, String accountNumber, String domainName) {
        return String.format(
                "arn:%s:es:%s:%s:domain/%s",
                partition,
                regionName,
                accountNumber,
                domainName
        );
    }
}