package de.widdix.awscftemplates;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.AmazonCloudFormationClientBuilder;
import com.amazonaws.services.cloudformation.model.*;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

public abstract class ACloudFormationTest extends AAWSTest {

    public static String readFile(String path, Charset encoding) {
        try {
            byte[] encoded = Files.readAllBytes(Paths.get(path));
            return new String(encoded, encoding);
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    private AmazonCloudFormation cf = AmazonCloudFormationClientBuilder.standard().withCredentials(this.credentialsProvider).build();

    private AmazonS3 s3 = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).build();

    public ACloudFormationTest() {
        super();
    }

    protected final void createStack(final String stackName, final String template, final Parameter...parameters) {
        CreateStackRequest req = new CreateStackRequest()
                .withStackName(stackName)
                .withParameters(parameters)
                .withCapabilities(Capability.CAPABILITY_IAM);
        if (Config.has(Config.Key.TEMPLATE_DIR)) {
            final String dir = Config.get(Config.Key.TEMPLATE_DIR);
            if (Config.has(Config.Key.BUCKET)) {
                final String bucket = Config.get(Config.Key.BUCKET);
                final String bucketLocation = s3.getBucketLocation(bucket);
                s3.putObject(bucket, stackName, new File(dir + template));
                req = req.withTemplateURL("https://s3-" + bucketLocation + ".amazonaws.com/" + bucket + "/" + stackName);
            } else {
                final String body = readFile(dir + template, Charset.forName("UTF-8"));
                req = req.withTemplateBody(body);
            }
        } else {
            req = req.withTemplateURL("https://s3-eu-west-1.amazonaws.com/widdix-aws-cf-templates/" + template);
        }
        this.cf.createStack(req);
    }

    protected enum FinalStatus {
        CREATE_COMPLETE(StackStatus.CREATE_COMPLETE, false, true, StackStatus.CREATE_IN_PROGRESS),
        DELETE_COMPLETE(StackStatus.DELETE_COMPLETE, true, false, StackStatus.DELETE_IN_PROGRESS);

        private final StackStatus finalStatus;
        private final boolean notFoundIsFinalStatus;
        private final boolean notFoundIsIntermediateStatus;
        private final Set<StackStatus> intermediateStatus;

        FinalStatus(StackStatus finalStatus, boolean notFoundIsFinalStatus, boolean notFoundIsIntermediateStatus, StackStatus...intermediateStatus) {
            this.finalStatus = finalStatus;
            this.notFoundIsFinalStatus = notFoundIsFinalStatus;
            this.notFoundIsIntermediateStatus = notFoundIsIntermediateStatus;
            this.intermediateStatus = new HashSet<>(Arrays.asList(intermediateStatus));
        }
    }

    private List<StackEvent> getStackEvents(final String stackName) {
        final List<StackEvent> events = new ArrayList<>();
        String nextToken = null;
        do {
            try {
                final DescribeStackEventsResult res = this.cf.describeStackEvents(new DescribeStackEventsRequest().withStackName(stackName).withNextToken(nextToken));
                events.addAll(res.getStackEvents());
                nextToken = res.getNextToken();
            } catch (final AmazonServiceException e) {
                if (e.getErrorMessage().equals("Stack [" + stackName + "] does not exist")) {
                    nextToken = null;
                } else {
                    throw e;
                }
            }
        } while (nextToken != null);
        Collections.reverse(events);
        return events;
    }

    protected final void waitForStack(final String stackName, final FinalStatus finalStackStatus) {
        System.out.println("waitForStack[" + stackName + "]: to reach status " + finalStackStatus.finalStatus);
        final List<StackEvent> eventsDisplayed = new ArrayList<>();
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (final InterruptedException e) {
                // continue
            }
            final List<StackEvent> events = getStackEvents(stackName);
            for (final StackEvent event : events) {
                boolean displayed = false;
                for (final StackEvent eventDisplayed : eventsDisplayed) {
                    if (event.getEventId().equals(eventDisplayed.getEventId())) {
                        displayed = true;
                    }
                }
                if (!displayed) {
                    System.out.println("waitForStack[" + stackName + "]: " + event.getTimestamp().toString() + " " + event.getLogicalResourceId() + " " + event.getResourceStatus() + " " + event.getResourceStatusReason());
                    eventsDisplayed.add(event);
                }
            }
            try {
                final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName));
                final StackStatus currentStatus = StackStatus.fromValue(res.getStacks().get(0).getStackStatus());
                if (finalStackStatus.finalStatus == currentStatus) {
                    System.out.println("waitForStack[" + stackName + "]: final status reached.");
                    return;
                } else {
                    if (finalStackStatus.intermediateStatus.contains(currentStatus)) {
                        System.out.println("waitForStack[" + stackName + "]: continue to wait (still in intermediate status " + currentStatus + ") ...");
                    } else {
                        throw new RuntimeException("waitForStack[" + stackName + "]: reached invalid intermediate status " + currentStatus + ".");
                    }
                }
            } catch (final AmazonServiceException e) {
                if (e.getErrorMessage().equals("Stack with id " + stackName + " does not exist")) {
                    if (finalStackStatus.notFoundIsFinalStatus) {
                        System.out.println("waitForStack[" + stackName + "]: final  reached (not found).");
                        return;
                    } else {
                        if (finalStackStatus.notFoundIsIntermediateStatus) {
                            System.out.println("waitForStack[" + stackName + "]: continue to wait (stack not found) ...");
                        } else {
                            throw new RuntimeException("waitForStack[" + stackName + "]: stack not found.");
                        }
                    }
                } else {
                    throw e;
                }
            }
        }
    }

    protected final Map<String, String> getStackOutputs(final String stackName) {
        final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName));
        final List<Output> outputs = res.getStacks().get(0).getOutputs();
        final Map<String, String> map = new HashMap<>(outputs.size());
        for (final Output output : outputs) {
            map.put(output.getOutputKey(), output.getOutputValue());
        }
        return map;
    }

    protected final String getStackOutputValue(final String stackName, final String outputKey) {
        return this.getStackOutputs(stackName).get(outputKey);
    }

    protected final void deleteStack(final String stackName) {
        this.cf.deleteStack(new DeleteStackRequest().withStackName(stackName));
        if (Config.has(Config.Key.BUCKET)) {
            final String bucket = Config.get(Config.Key.BUCKET);
            s3.deleteObject(bucket, stackName);
        }
    }

}

@@ -77,46 +77,8 @@
 on;%0A
-import com.amazonaws.regions.Regions;%0A
 impo
@@ -1802,16 +1802,21 @@
 ation = 
+this.
 s3.getBu
@@ -1853,18 +1853,175 @@
         
-s3
+final AmazonS3 s3local = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).withRegion(bucketLocation).build();%0A                s3local
 .putObje
@@ -8217,18 +8217,248 @@
         
-s3
+final String bucketLocation = this.s3.getBucketLocation(bucket);%0A            final AmazonS3 s3local = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).withRegion(bucketLocation).build();%0A            s3local
 .deleteO

create local client