/*
 *
 *  Copyright 2012 Netflix, Inc.
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 *
 */

package com.netflix.simianarmy.aws;

import java.util.*;

import org.apache.commons.lang.Validate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import com.netflix.simianarmy.NamedType;
import com.netflix.simianarmy.Resource;
import com.netflix.simianarmy.ResourceType;

/**
 * The class represents general AWS resources that are managed by janitor monkey.
 */
public class AWSResource implements Resource {
    private String id;
    private ResourceType resourceType;
    private String region;
    private String ownerEmail;
    private String description;
    private String terminationReason;
    private CleanupState state;
    private Date expectedTerminationTime;
    private Date actualTerminationTime;
    private Date notificationTime;
    private Date launchTime;
    private Date markTime;
    private boolean optOutOfJanitor;
    private String awsResourceState;

    /** The field name for resourceId. **/
    public static final String FIELD_RESOURCE_ID = "resourceId";
    /** The field name for resourceType. **/
    public static final String FIELD_RESOURCE_TYPE = "resourceType";
    /** The field name for region. **/
    public static final String FIELD_REGION = "region";
    /** The field name for owner email. **/
    public static final String FIELD_OWNER_EMAIL = "ownerEmail";
    /** The field name for description. **/
    public static final String FIELD_DESCRIPTION = "description";
    /** The field name for state. **/
    public static final String FIELD_STATE = "state";
    /** The field name for terminationReason. **/
    public static final String FIELD_TERMINATION_REASON = "terminationReason";
    /** The field name for expectedTerminationTime. **/
    public static final String FIELD_EXPECTED_TERMINATION_TIME = "expectedTerminationTime";
    /** The field name for actualTerminationTime. **/
    public static final String FIELD_ACTUAL_TERMINATION_TIME = "actualTerminationTime";
    /** The field name for notificationTime. **/
    public static final String FIELD_NOTIFICATION_TIME = "notificationTime";
    /** The field name for launchTime. **/
    public static final String FIELD_LAUNCH_TIME = "launchTime";
    /** The field name for markTime. **/
    public static final String FIELD_MARK_TIME = "markTime";
    /** The field name for isOptOutOfJanitor. **/
    public static final String FIELD_OPT_OUT_OF_JANITOR = "optOutOfJanitor";
    /** The field name for awsResourceState. **/
    public static final String FIELD_AWS_RESOURCE_STATE = "awsResourceState";

    /** The date format used to print or parse a Date value. **/
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss");

    /** The map from name to value for additional fields used by the resource. **/
    private final Map<String, String> additionalFields = new HashMap<String, String>();

    /** The map from AWS tag key to value for the resource. **/
    private final Map<String, String> tags = new HashMap<String, String>();

    /** {@inheritDoc} */
    @Override
    public Map<String, String> getFieldToValueMap() {
        Map<String, String> fieldToValue = new HashMap<String, String>();

        putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_ID, getId());
        putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_TYPE, getResourceType());
        putToMapIfNotNull(fieldToValue, FIELD_REGION, getRegion());
        putToMapIfNotNull(fieldToValue, FIELD_OWNER_EMAIL, getOwnerEmail());
        putToMapIfNotNull(fieldToValue, FIELD_DESCRIPTION, getDescription());
        putToMapIfNotNull(fieldToValue, FIELD_STATE, getState());
        putToMapIfNotNull(fieldToValue, FIELD_TERMINATION_REASON, getTerminationReason());
        putToMapIfNotNull(fieldToValue, FIELD_EXPECTED_TERMINATION_TIME, printDate(getExpectedTerminationTime()));
        putToMapIfNotNull(fieldToValue, FIELD_ACTUAL_TERMINATION_TIME, printDate(getActualTerminationTime()));
        putToMapIfNotNull(fieldToValue, FIELD_NOTIFICATION_TIME, printDate(getNotificationTime()));
        putToMapIfNotNull(fieldToValue, FIELD_LAUNCH_TIME, printDate(getLaunchTime()));
        putToMapIfNotNull(fieldToValue, FIELD_MARK_TIME, printDate(getMarkTime()));
        putToMapIfNotNull(fieldToValue, FIELD_AWS_RESOURCE_STATE, getAWSResourceState());

        // Additional fields are serialized while tags are not. So if any tags need to be
        // serialized as well, put them to additional fields.
        fieldToValue.put(FIELD_OPT_OUT_OF_JANITOR, String.valueOf(isOptOutOfJanitor()));

        fieldToValue.putAll(additionalFields);

        return fieldToValue;
    }

    /**
     * Parse a map from field name to value to a resource.
     * @param fieldToValue the map from field name to value
     * @return the resource that is de-serialized from the map
     */
    public static AWSResource parseFieldtoValueMap(Map<String, String> fieldToValue) {
        AWSResource resource = new AWSResource();
        for (Map.Entry<String, String> field : fieldToValue.entrySet()) {
            String name = field.getKey();
            String value = field.getValue();
            if (name.equals(FIELD_RESOURCE_ID)) {
                resource.setId(value);
            } else if (name.equals(FIELD_RESOURCE_TYPE)) {
                resource.setResourceType(AWSResourceType.valueOf(value));
            } else if (name.equals(FIELD_REGION)) {
                resource.setRegion(value);
            } else if (name.equals(FIELD_OWNER_EMAIL)) {
                resource.setOwnerEmail(value);
            } else if (name.equals(FIELD_DESCRIPTION)) {
                resource.setDescription(value);
            } else if (name.equals(FIELD_STATE)) {
                resource.setState(CleanupState.valueOf(value));
            } else if (name.equals(FIELD_TERMINATION_REASON)) {
                resource.setTerminationReason(value);
            } else if (name.equals(FIELD_EXPECTED_TERMINATION_TIME)) {
                resource.setExpectedTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
            } else if (name.equals(FIELD_ACTUAL_TERMINATION_TIME)) {
                resource.setActualTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
            } else if (name.equals(FIELD_NOTIFICATION_TIME)) {
                resource.setNotificationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
            } else if (name.equals(FIELD_LAUNCH_TIME)) {
                resource.setLaunchTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
            } else if (name.equals(FIELD_MARK_TIME)) {
                resource.setMarkTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
            } else if (name.equals(FIELD_AWS_RESOURCE_STATE)) {
                resource.setAWSResourceState(value);
            } else if (name.equals(FIELD_OPT_OUT_OF_JANITOR)) {
                resource.setOptOutOfJanitor("true".equals(value));
            } else {
                // put all other fields into additional fields
                resource.setAdditionalField(name, value);
            }
        }
        return resource;
    }

    public String getAWSResourceState() {
        return awsResourceState;
    }

    public void setAWSResourceState(String awsState) {
        this.awsResourceState = awsState;
    }

    /** {@inheritDoc} */
    @Override
    public String getId() {
        return id;
    }

    /** {@inheritDoc} */
    @Override
    public void setId(String id) {
        this.id = id;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withId(String resourceId) {
        setId(resourceId);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public ResourceType getResourceType() {
        return resourceType;
    }

    /** {@inheritDoc} */
    @Override
    public void setResourceType(ResourceType resourceType) {
        this.resourceType = resourceType;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withResourceType(ResourceType type) {
        setResourceType(type);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public String getRegion() {
        return region;
    }

    /** {@inheritDoc} */
    @Override
    public void setRegion(String region) {
        this.region = region;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withRegion(String resourceRegion) {
        setRegion(resourceRegion);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public String getOwnerEmail() {
        return ownerEmail;
    }

    /** {@inheritDoc} */
    @Override
    public void setOwnerEmail(String ownerEmail) {
        this.ownerEmail = ownerEmail;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withOwnerEmail(String resourceOwner) {
        setOwnerEmail(resourceOwner);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public String getDescription() {
        return description;
    }

    /** {@inheritDoc} */
    @Override
    public void setDescription(String description) {
        this.description = description;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withDescription(String resourceDescription) {
        setDescription(resourceDescription);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Date getLaunchTime() {
        return getCopyOfDate(launchTime);
    }

    /** {@inheritDoc} */
    @Override
    public void setLaunchTime(Date launchTime) {
        this.launchTime = getCopyOfDate(launchTime);
    }

    /** {@inheritDoc} */
    @Override
    public Resource withLaunchTime(Date resourceLaunchTime) {
        setLaunchTime(resourceLaunchTime);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Date getMarkTime() {
        return getCopyOfDate(markTime);
    }

    /** {@inheritDoc} */
    @Override
    public void setMarkTime(Date markTime) {
        this.markTime = getCopyOfDate(markTime);
    }

    /** {@inheritDoc} */
    @Override
    public Resource withMarkTime(Date resourceMarkTime) {
        setMarkTime(resourceMarkTime);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Date getExpectedTerminationTime() {
        return getCopyOfDate(expectedTerminationTime);
    }

    /** {@inheritDoc} */
    @Override
    public void setExpectedTerminationTime(Date expectedTerminationTime) {
        this.expectedTerminationTime = getCopyOfDate(expectedTerminationTime);
    }

    /** {@inheritDoc} */
    @Override
    public Resource withExpectedTerminationTime(Date resourceExpectedTerminationTime) {
        setExpectedTerminationTime(resourceExpectedTerminationTime);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Date getActualTerminationTime() {
        return getCopyOfDate(actualTerminationTime);
    }

    /** {@inheritDoc} */
    @Override
    public void setActualTerminationTime(Date actualTerminationTime) {
        this.actualTerminationTime = getCopyOfDate(actualTerminationTime);
    }

    /** {@inheritDoc} */
    @Override
    public Resource withActualTerminationTime(Date resourceActualTerminationTime) {
        setActualTerminationTime(resourceActualTerminationTime);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Date getNotificationTime() {
        return getCopyOfDate(notificationTime);
    }

    /** {@inheritDoc} */
    @Override
    public void setNotificationTime(Date notificationTime) {
        this.notificationTime = getCopyOfDate(notificationTime);
    }

    /** {@inheritDoc} */
    @Override
    public Resource withNnotificationTime(Date resourceNotificationTime) {
        setNotificationTime(resourceNotificationTime);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public CleanupState getState() {
        return state;
    }

    /** {@inheritDoc} */
    @Override
    public void setState(CleanupState state) {
        this.state = state;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withState(CleanupState resourceState) {
        setState(resourceState);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public String getTerminationReason() {
        return terminationReason;
    }

    /** {@inheritDoc} */
    @Override
    public void setTerminationReason(String terminationReason) {
        this.terminationReason = terminationReason;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withTerminationReason(String resourceTerminationReason) {
        setTerminationReason(resourceTerminationReason);
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isOptOutOfJanitor() {
        return optOutOfJanitor;
    }

    /** {@inheritDoc} */
    @Override
    public void setOptOutOfJanitor(boolean optOutOfJanitor) {
        this.optOutOfJanitor = optOutOfJanitor;
    }

    /** {@inheritDoc} */
    @Override
    public Resource withOptOutOfJanitor(boolean optOut) {
        setOptOutOfJanitor(optOut);
        return this;
    }

    private static Date getCopyOfDate(Date date) {
        if (date == null) {
            return null;
        }
        return new Date(date.getTime());
    }

    private static void putToMapIfNotNull(Map<String, String> map, String key, String value) {
        Validate.notNull(map);
        Validate.notNull(key);
        if (value != null) {
            map.put(key, value);
        }
    }

    private static void putToMapIfNotNull(Map<String, String> map, String key, Enum<?> value) {
        Validate.notNull(map);
        Validate.notNull(key);
        if (value != null) {
            map.put(key, value.name());
        }
    }

    private static void putToMapIfNotNull(Map<String, String> map, String key, NamedType value) {
        Validate.notNull(map);
        Validate.notNull(key);
        if (value != null) {
            map.put(key, value.name());
        }
    }

    private static String printDate(Date date) {
        if (date == null) {
            return null;
        }

        return DATE_FORMATTER.print(date.getTime());
    }

    @Override
    public Resource setAdditionalField(String fieldName, String fieldValue) {
        Validate.notNull(fieldName);
        Validate.notNull(fieldValue);
        putToMapIfNotNull(additionalFields, fieldName, fieldValue);
        return this;
    }

    @Override
    public String getAdditionalField(String fieldName) {
        return additionalFields.get(fieldName);
    }

    @Override
    public Collection<String> getAdditionalFieldNames() {
        return additionalFields.keySet();
    }

    @Override
    public Resource cloneResource() {
        Resource clone = new AWSResource()
        .withActualTerminationTime(getActualTerminationTime())
        .withDescription(getDescription())
        .withExpectedTerminationTime(getExpectedTerminationTime())
        .withId(getId())
        .withLaunchTime(getLaunchTime())
        .withMarkTime(getMarkTime())
        .withNnotificationTime(getNotificationTime())
        .withOwnerEmail(getOwnerEmail())
        .withRegion(getRegion())
        .withResourceType(getResourceType())
        .withState(getState())
        .withTerminationReason(getTerminationReason())
        .withOptOutOfJanitor(isOptOutOfJanitor());
        ((AWSResource) clone).setAWSResourceState(awsResourceState);

        ((AWSResource) clone).additionalFields.putAll(additionalFields);

        for (String key : this.getAllTagKeys()) {
            clone.setTag(key, this.getTag(key));
        }

        return clone;
    }

    /** {@inheritDoc} */
    @Override
    public void setTag(String key, String value) {
        tags.put(key, value);
    }

    /** {@inheritDoc} */
    @Override
    public String getTag(String key) {
        return tags.get(key);
    }

    /** {@inheritDoc} */
    @Override
    public Collection<String> getAllTagKeys() {
        return tags.keySet();
    }

    @Override
    public String toString() {
        return "AWSResource{" +
                "id='" + id + '\'' +
                ", resourceType=" + resourceType +
                ", region='" + region + '\'' +
                ", ownerEmail='" + ownerEmail + '\'' +
                ", description='" + description + '\'' +
                ", terminationReason='" + terminationReason + '\'' +
                ", state=" + state +
                ", expectedTerminationTime=" + expectedTerminationTime +
                ", actualTerminationTime=" + actualTerminationTime +
                ", notificationTime=" + notificationTime +
                ", launchTime=" + launchTime +
                ", markTime=" + markTime +
                ", optOutOfJanitor=" + optOutOfJanitor +
                ", awsResourceState='" + awsResourceState + '\'' +
                ", additionalFields=" + additionalFields +
                ", tags=" + tags +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        // consider two resources to be equivalent if id, resourceType and region match
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AWSResource that = (AWSResource) o;
        return Objects.equals(id, that.id) &&
                Objects.equals(resourceType, that.resourceType) &&
                Objects.equals(region, that.region);
    }

    @Override
    public int hashCode() {
        // consider two resources to be equivalent if id, resourceType and region match
        return Objects.hash(id, resourceType, region);
    }
}
