/*
 * Licensed Materials - Property of HCL
 * UrbanCode Deploy
 * (c) Copyright HCL Technologies Ltd. 2019. All Rights Reserved.
 */
package com.urbancode.urelease.integration.xldeploy

import com.urbancode.release.rest.models.Application
import com.urbancode.release.rest.models.Inventory
import com.urbancode.release.rest.models.internal.ApplicationEnvironment
import com.urbancode.release.rest.models.internal.Comment
import com.urbancode.release.rest.models.internal.PluginIntegrationProvider
import com.urbancode.release.rest.models.internal.TaskExecution
import com.urbancode.release.rest.models.internal.TaskPlan
import com.urbancode.release.rest.models.internal.Task.TaskType
import com.urbancode.release.rest.models.Version

import com.urbancode.urelease.integration.xldeploy.config.XLConfigType
import com.urbancode.urelease.integration.xldeploy.config.XLTaskState
import com.urbancode.urelease.integration.xldeploy.hash.HashUtility
import com.urbancode.urelease.integration.xldeploy.http.UCRClient
import com.urbancode.urelease.integration.xldeploy.http.XLRestClient
import com.urbancode.urelease.integration.xldeploy.sync.FullSyncClient
import com.urbancode.urelease.integration.xldeploy.sync.InventorySyncClient
import com.urbancode.urelease.integration.xldeploy.sync.SyncClient

import java.util.concurrent.TimeUnit

import org.apache.log4j.Logger
import org.codehaus.jettison.json.JSONArray
import org.codehaus.jettison.json.JSONObject
import org.joda.time.DateTime
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormatter
import org.joda.time.format.ISODateTimeFormat

/**
 * Class used to import XL Deploy objects into UCR.
 */
public class Integration extends IntegrationBase {
    final String DATE_FORMAT = "YYYY-MM-dd'T'HH:mm:ss.sZ" // XL requires ISO 8601 date format
    final int MAX_RESULTS = 500 // Max number of query results
    final int DEFAULT_FULL_SYNC = 24 // Default full sync interval when one isn't provided

    String lastExecution // Kept empty if full sync
    int syncInterval // Interval between full syncs
    boolean fullSync // Will determine between delta sync and full sync

    public Integration(Properties props) {
        super(props)

        /* Use default interval if one isn't provided or is invalid */
        if (props['syncInterval']) {
            try {
                syncInterval = Integer.parseInt(props['syncInterval'])
            }
            catch (NumberFormatException ex) {
                logger.warn("Sync interval ${props['syncInterval']} is invalid. " +
                    "Defaulting to ${DEFAULT_FULL_SYNC} hours.")
                syncInterval = DEFAULT_FULL_SYNC
            }
        }
        else {
            syncInterval = DEFAULT_FULL_SYNC
        }
    }

    /*
     * @see com.urbancode.urelease.integration.xldeploy.IntegrationBase#init()
     */
    @Override
    public void init () {
        /* We retrieve the full provider object */
        provider = new PluginIntegrationProvider().id(integrationProviderId).get()

        /* We need to make sure that all objects imported from that integration won't be editable */
        provider.disabledFields("change", "type", "name", "status", "release", "application",
            "description", "severity", "initiative")
        provider.disabledFields("initiative", "description", "name")

        provider.save()

        /* Never execute a full sync when the sync interval is set to -1 */
        if (syncInterval != -1) {
            String lastFullSync = provider.getProperty("lastFullSync")

            if (lastFullSync != null) {
                DateTimeFormatter parser = ISODateTimeFormat.dateTime()

                /* Check if 24 hours have passed since last full sync */
                DateTime lastSyncDate = parser.parseDateTime(lastFullSync)
                DateTime currentDate = new DateTime()

                long millisDiff = currentDate.getMillis() - lastSyncDate.getMillis()
                long hourDiff = TimeUnit.MILLISECONDS.toHours(millisDiff)

                if (hourDiff >= syncInterval) {
                    fullSync = true
                    logger.info("Executing full sync since ${hourDiff} hours have elapsed, " +
                        "exceeding the ${syncInterval} hour full sync interval.")
                }
                else {
                    fullSync = false
                    lastExecution = provider.getProperty("lastExecution")
                    logger.info("Executing a delta sync since ${hourDiff} hours have elapsed, "
                        + "and the full sync interval is set to ${syncInterval} hours.")
                }
            }
            else {
                fullSync = true
                logger.info("A full sync has never been executed. Running full sync now.")
            }
        }
        else {
            logger.info("Executing full sync since the full sync interval is set to -1.")
        }
    }

    /**
     * Run the integration and import all apps, environments, versions, and tasks from XL Deploy.
     */
    public void runIntegration () {
        init() // Load the integration provider
        checkRunningTasks() // Always check for running tasks that can be completed

        SyncClient<Application> appSyncClient = null
        SyncClient<ApplicationEnvironment> envSyncClient = null
        SyncClient<Version> versionSyncClient = null
        InventorySyncClient invSyncClient = new InventorySyncClient(MAX_RESULTS)

        if (fullSync) {
            appSyncClient = new FullSyncClient<Application>(new Application(), MAX_RESULTS,
                    integrationProviderId)
            envSyncClient = new FullSyncClient<ApplicationEnvironment>(new ApplicationEnvironment(),
                MAX_RESULTS, integrationProviderId)
            versionSyncClient = new FullSyncClient<Version>(new Version(), MAX_RESULTS,
                    integrationProviderId)
        }
        else {
            appSyncClient = new SyncClient<Application>(new Application(), MAX_RESULTS)
            envSyncClient = new SyncClient<ApplicationEnvironment>(new ApplicationEnvironment(),
                MAX_RESULTS)
            versionSyncClient = new SyncClient<Version>(new Version(), MAX_RESULTS)
        }

        List<Application> newUcrApps = importApps(appSyncClient)
        importVersions(versionSyncClient)
        importAppEnvs(envSyncClient)
        importProcesses(newUcrApps)
        importInventories(invSyncClient)

        /* Sync removals in reverse order */
        appSyncClient.syncRemovals()
        versionSyncClient.syncRemovals()
        envSyncClient.syncRemovals()

        logger.info("Imported ${appSyncClient.importCount} application(s).")
        logger.info("Deleted ${appSyncClient.removalCount} application(s).")
        logger.info("Imported ${envSyncClient.importCount} application environment(s).")
        logger.info("Deleted ${envSyncClient.removalCount} application environment(s).")
        logger.info("Imported ${versionSyncClient.importCount} version(s).")
        logger.info("Deleted ${versionSyncClient.removalCount} version(s).")
        logger.info("Imported ${invSyncClient.importCount} inventory entr(y/ies).")

        /* Update the last execution time of the integration provider */
        String currTime = new DateTime().toString(DATE_FORMAT)
        provider.property("lastExecution", currTime).save()
        if (fullSync) {
            provider.property("lastFullSync", currTime).save()
        }
    }

    /**
     * Check for any tasks to complete in UCR that have finished in XL Deploy.
     */
    private void checkRunningTasks() {
        List<TaskExecution> taskExecutions =
            new TaskExecution().getAllExecutingForProvider(integrationProviderId)

        for (TaskExecution taskExecution : taskExecutions) {
            String xlTaskIds = taskExecution.getProperty("xlTaskIds")

            boolean complete = true
            for (String xlTaskId : xlTaskIds?.tokenize(",")) {
                String taskState = xlClient.getTaskState(xlTaskId)
                XLTaskState state = new XLTaskState(taskState)

                if (state.running) {
                    complete = false
                    break
                }
                else if (state.failed) {
                    complete  = true
                    new Comment().task(taskExecution)
                        .comment("Task execution has failed due to XL Deploy Task ${xlTaskId}.")
                        .post()
                    taskExecution.fail()
                    taskExecution.hasFailed = true
                    break
                }
                else {
                    new Comment().task(taskExecution)
                        .comment("XL Deploy task ${xlTaskId} has succeeded.")
                        .post()

                    // Do not try to archive tasks that have already been archived
                    if (!state.done) {
                        new Comment().task(taskExecution)
                            .comment("Archiving XL Deploy task with ID ${xlTaskId}.")
                            .post()

                        xlClient.archiveTask(xlTaskId)
                    }
                }
            }

            /* Complete task if no XL Deploy tasks are still running or have failed */
            if (complete && !taskExecution.hasFailed) {
                new Comment().task(taskExecution)
                    .comment("All XL Deploy tasks completed successfully.")
                    .post()
                taskExecution.complete()
            }
        }
    }

    /**
     * Import any applications from XL Deploy into UCR.
     * @param syncClient The client used to import XL Deploy applications.
     * @return A list of all UCR applications that have been synced.
     */
    private List<Application> importApps(SyncClient<Application> syncClient) {
        List<Application> newUcrApps = new ArrayList<Application>()

        JSONArray xlApps = null
        int page = 0
        /* Loop through each page of results */
        while (xlApps == null || xlApps.length() == MAX_RESULTS) {
            xlApps = xlClient.getConfigObjects(XLConfigType.APPLICATION, lastExecution, page,
                MAX_RESULTS)

            for (int i = 0; i < xlApps.length(); i++) {
                JSONObject xlApp = xlApps.getJSONObject(i)
                String ref = xlApp.getString("ref")
                String name = ref.substring(ref.lastIndexOf("/") + 1)
                String externalId = HashUtility.hashReference(ref) // Create unique ID

                Application ucrApp = new Application()
                    .name(name)
                    .automated(true)
                    .property("objectType", "APPLICATION")
                    .integrationProvider(provider)
                    .externalId(externalId)
                    .externalUrl(ref)

                newUcrApps.add(ucrApp)
                syncClient.addImport(ucrApp)
            }

            page++
        }

        syncClient.syncImports() // Sync any remaining applications
        return newUcrApps
    }

    /**
     * Import each environment from XLDeploy as a target of each UCR application.
     * @param syncClient The client to import all app environments from XL Deploy into UCR.
     * @param ucrApps List of all changed UCR apps for this integration.
     */
    private void importAppEnvs(SyncClient<ApplicationEnvironment> syncClient)
    {
        List<Application> ucrApps = ucrClient.getApps(provider.id) // All UCR applications
        JSONArray xlEnvs = null
        int page = 0

        /* Continue if xlEnvs haven't been queried for or if there is another page */
        while (xlEnvs == null || xlEnvs.length() == MAX_RESULTS) {
            xlEnvs = xlClient.getConfigObjects(XLConfigType.ENVIRONMENT, lastExecution, page,
                MAX_RESULTS)

            for (Application ucrApp : ucrApps) {
                /* Create all environments for each application */
                for (int i = 0; i < xlEnvs.length(); i++) {
                    JSONObject xlEnv = xlEnvs.getJSONObject(i)
                    String ref = xlEnv.getString("ref") // Used to access from XL Deploy API
                    String name = ref.substring(ref.lastIndexOf("/") + 1)

                    /* Create a unique reference for application/environment */
                    String uniqueRef = ucrApp.externalUrl + "/" + ref
                    String externalId = HashUtility.hashReference(uniqueRef)

                    ApplicationEnvironment ucrAppEnv = new ApplicationEnvironment()
                        .name(name)
                        .integrationProvider(provider)
                        .externalId(externalId)
                        .externalUrl(ref)

                    Application parent = new Application().externalId(ucrApp.getExternalId())
                    ucrAppEnv.application(parent)

                    syncClient.addImport(ucrAppEnv)
                }
            }

            page++
        }

        syncClient.syncImports() // sync any remaining environments
    }

    /**
     * Import XL Deploy application version as UCR application versions.
     * @param syncClient Client to import version from XL Deploy into UCR.
     */
    private void importVersions(SyncClient<Version> syncClient) {
        JSONArray xlVersions
        int page = 0
        /* Continue if xlVersions hasn't been queried for or there is another page */
        while (xlVersions == null || xlVersions.length() == MAX_RESULTS) {
            xlVersions = xlClient.getConfigObjects(XLConfigType.VERSION, lastExecution, page,
                MAX_RESULTS)

            for (int i = 0; i < xlVersions.length(); i++) {
                JSONObject xlVersion = xlVersions.getJSONObject(i)
                String ref = xlVersion.getString("ref")
                String name = ref.substring(ref.lastIndexOf("/") + 1)
                String externalId = HashUtility.hashReference(ref)

                Version ucrVersion = new Version()
                    .name(name)
                    .automated(true)
                    .integrationProvider(provider)
                    .externalId(externalId)
                    .externalUrl(ref)

                JSONObject xlVersionObj = xlClient.getConfigObject(ref)
                String xlAppRef= xlVersionObj.getString("application")

                if (xlAppRef != null) {
                    String ucrAppId = HashUtility.hashReference(xlAppRef)
                    ucrVersion.application(new Application().externalId(ucrAppId))
                }

                syncClient.addImport(ucrVersion)
            }

            page++
        }

        syncClient.syncImports() // sync any remaining versions
    }

    /**
     * Sync the environment inventories from UCR with the inventories in XL Deploy.
     * @param syncClient
     */
    private void importInventories(InventorySyncClient syncClient) {
        JSONArray xlDeployedApps
        int page = 0

        while (xlDeployedApps == null || xlDeployedApps.length() == MAX_RESULTS) {
            xlDeployedApps = xlClient.getConfigObjects(XLConfigType.DEPLOYEDAPP, lastExecution, page,
                MAX_RESULTS)

            for (int i = 0; i < xlDeployedApps.length(); i++) {
                JSONObject xlDeployedAppMeta = xlDeployedApps.getJSONObject(i)
                String ref = xlDeployedAppMeta.getString("ref")
                String name = ref.substring(ref.lastIndexOf("/") + 1)
                String id = HashUtility.hashReference(ref)

                JSONObject xlDeployedApp = xlClient.getConfigObject(ref)
                String versionRef = xlDeployedApp.getString("version")
                String versionId = HashUtility.hashReference(versionRef)
                String envRef = xlDeployedApp.getString("environment")
                String appRef = versionRef.substring(0, versionRef.lastIndexOf("/"))
                String envId = HashUtility.hashReference(appRef + "/" + envRef)

                Inventory inventory = new Inventory()
                inventory.applicationTarget(new ApplicationEnvironment().externalId(envId))
                inventory.version(new Version().externalId(versionId))
                inventory.id(id)
                syncClient.addImport(inventory)
            }

            page++
        }

        syncClient.syncImports() // sync any remaining inventories
    }

    /**
     * Create a new 'Deploy' task for each UCR application.
     * @param ucrApps All changed UCR applications.
     */
    private void importProcesses(List<Application> ucrApps) {
        List<TaskPlan> taskPlans = new ArrayList<TaskPlan>()
        int imported = 0

        for (Application ucrApp : ucrApps) {
            ucrApp.format("externalIntegrationDetails");
            String externalId = HashUtility.hashReference(ucrApp.name + "/deploy")
            TaskPlan process = new TaskPlan()
                    .name("Deploy " + ucrApp.getName())
                    .integrationProvider(provider)
                    .externalId(externalId)
                    .executionStep("ExecuteTask")
                    .taskType(TaskType.PluginTask)
                    .active(true)
                    .automated(true)
                    .application(ucrApp)
            taskPlans.add(process)

            if (taskPlans.size() == MAX_RESULTS) {
                List<TaskPlan> importedTaskPlans = new TaskPlan().sync(taskPlans)
                taskPlans.clear()
                imported += importedTaskPlans.size()
            }
        }

        if (taskPlans.size() > 0) {
            List<TaskPlan> importedTaskPlans = new TaskPlan().sync(taskPlans)
            imported += importedTaskPlans.size()
        }
    }
}
