define('bitbucket/internal/import-form', [
    'aui',
    'jquery',
    'lodash',
    'bitbucket/util/navbuilder',
    'bitbucket/util/server',
    'bitbucket/internal/importer/owner-selector',
    'bitbucket/internal/importer/repository-list',
    'bitbucket/internal/util/analytics',
    'bitbucket/internal/util/feature-detect',
    'bitbucket/internal/util/history',
    'bitbucket/internal/util/notifications',
    'exports'
], function(
    AJS,
    $,
    _,
    navBuilder,
    server,
    ownerSelector,
    RepositoryList,
    analytics,
    feature,
    history,
    notifications,
    exports
) {

    'use strict';
    var $formBody;
    var repoList;

    var Stages = {
        LANDING_PAGE: 'landing-page',
        SELECT_REPOSITORY: 'select-repository',
        IMPORTING: 'importing'
    };

    var RepositorySelectionMode = {
        ALL: 'all',
        SELECT: 'select-repositories'
    };

    // This MUST match the enum defined in java/com/atlassian/bitbucket/internal/importer/SourceType.java
    var Sources = {
        BITBUCKET_CLOUD: 'BITBUCKET_CLOUD',
        GIT: 'GIT',
        GITHUB: 'GITHUB'
    };

    // This MUST match the enum defined in java/com/atlassian/bitbucket/internal/importer/rest/ResponseError.java
    var ErrorStates = {
        EXTERNAL_REQUEST: 'EXTERNAL_REQUEST',
        AUTHENTICATION: 'AUTHENTICATION',
        UNSUPPORTED_SOURCE: 'UNSUPPORTED_SOURCE',
        FORBIDDEN: 'FORBIDDEN'
    };

    // This MUST match the enum defined in java/com/atlassian/bitbucket/internal/importer/repository/ImportTaskState.java
    var ImportState = {
        FAILED: 'FAILED',
        QUEUED: 'QUEUED',
        STARTED: 'STARTED',
        SUCCESS: 'SUCCESS'
    };

    // This MUST match the enum defined in java/com/atlassian/bitbucket/internal/importer/repository/ImportTaskFailureType.java
    var FailureTypes = {
        CREATE_DUPLICATE: 'CREATE_DUPLICATE'
    };

    var state = {
        Stages: Stages,
        RepositorySelectionMode: RepositorySelectionMode,
        Sources: Sources,

        stage: Stages.LANDING_PAGE,
        source: {
            url: null,
            type: Sources.BITBUCKET_CLOUD,
            error: null
        },
        credential: {
            username: null,
            password: null,
            error: null
        },
        project: {
            key: null,
            name: null
        },
        importing: {
            jobId: null,
            tasks: null
        },
        githubEnterprise: false,
        validForm: false,
        possibleOwners: [],
        owner: '',
        repos: [], // the list of selected repos in the shape {name: String, cloneUrl: String, scmId: String, description: String }
        repoSelectionMode: RepositorySelectionMode.ALL
    };

    var importPollingTimeout;
    var baseRestUrl = navBuilder.rest('importer', 'latest').addPathComponents('external-source');

    function rerender() {
        $formBody.html(bitbucket.internal.importer.renderWithState({ state: state }));
    }

    function rerenderConnect() {
        $formBody.find('.next-step').replaceWith(bitbucket.internal.importer.connect({ state: state }));
    }

    function rerenderRepoListButtons() {
        $formBody.find('.next-step').replaceWith(bitbucket.internal.importer.repositoryListButtons({ state: state }));
    }

    function rerenderGithub() {
        $formBody.find('.github-specific').replaceWith(bitbucket.internal.importer.githubSpecific({ state: state }));
    }

    function rerenderGitUrl() {
        $formBody.find('.git-url').replaceWith(bitbucket.internal.importer.gitUrl({ state: state }));
    }

    /**
     * Toggle if a button has a spinner displayed or not. If the spinner is displayed the button is disabled
     * @param {jQuery element} $spinnerButton
     * @param {boolean} toggle
     */
    function spinnerButtonToggle($spinnerButton, toggle) {
        if (toggle || (toggle == null && !$spinnerButton.hasClass('spinning'))) {
            if ($spinnerButton.hasClass('spinning')) {
                // already a request in flight - don't start another one.
                return;
            }
            $spinnerButton
                .addClass('spinning')
                .find('.spinner')
                .spin('small', {color: '#ffffff'});
            $spinnerButton.find('input').attr('disabled', 'true');
        } else {
            $spinnerButton
                .removeClass('spinning')
                .find('.spinner')
                .spinStop();
            $spinnerButton.find('input').removeAttr('disabled');
        }
    }

    function resetLandingPageState(newSourceType) {
        var resetStateObj = {
            source: {
                type: newSourceType,
                url: null,
                error: null
            },
            credential: {
                username: null,
                password: null,
                error: null
            },
            githubEnterprise: false,
            validForm: false
        };
        state = $.extend(true, state, resetStateObj);
        analytics.add("importer.form.initialised", {"sourceType": newSourceType});
    }

    function changeStage(newStage, dontAddHistory) {
        if (!dontAddHistory) {
            history.pushState(null, null, "#" + newStage);
        }

        analytics.add("importer.form.stage.changed", {
            "oldStage"          : state.stage,
            "newStage"          : newStage,
            "sourceType"        : state.source.type,
            "hasUrl"            : !(state.source.url === null),
            "jobId"             : state.importing.jobId,
            "tasks"             : state.importing.tasks ? state.importing.tasks.length : 0,
            "githubEnterprise"  : state.githubEnterprise,
            "possibleOwners"    : state.possibleOwners.length,
            "repos"             : state.repos.length,
            "repoSelectionMode" : state.repoSelectionMode
        });

        state = $.extend(true, state, {
            owner: '',
            repos: [],
            repoSelectionMode: RepositorySelectionMode.ALL
        });
        repoList = null;
        state.stage = newStage;
        rerender();
    }

    function requiredFieldsPresent() {
        if (state.source.type === Sources.GIT) {
            if (state.source.url && !state.source.url.match(/^https?:\/\/[^\/]+/)) {
                state.validForm = false;
                state.source.error = AJS.I18n.getText('bitbucket.importer.import.form.cloneUrl.error');
            } else {
                state.validForm = !!state.source.url; // if credentials are filled in before clone url there might not be a url
                state.source.error = null;
                // Every repo needs a name. The other sources provide us names when the user selects the specific
                // repos they would like to import. For Git we need to supply a name explicitly
                // Find everything after the last / that isn't trailing
                // Remove any trailing .git if present
                state.source.name = state.source.url && state.source.url.match(/\/([^\/]+)\/?$/)[1].replace(/\.git$/, '');
                state.repos[0] = {
                    cloneUrl: state.source.url,
                    name: state.source.name,
                    description: AJS.I18n.getText('bitbucket.importer.import.form.git.repository.description', state.source.url),
                    scmId: 'git'
                };

            }
            rerenderGitUrl();
        } else {
            state.validForm = !!(state.credential.username && state.credential.password);

            if (state.source.type === Sources.GITHUB) {
                state.validForm = state.validForm && (state.githubEnterprise === !!state.source.url);
            }
        }
    }

    function getOwner() {
        state.validForm = true;
        state.source.error = null;
        state.credential.error = null;

        var baseRestRequestOptions = {
            contextType: 'application/json',
            type: 'POST',
            data: {
                credential: state.credential,
                source: state.source
            }
        };

        function getOrganisations() {
            var url = baseRestUrl
                .addPathComponents('owners')
                // request a large limit to avoid needing to make multiple requests to get owners
                // the < 100 case should be the majority case
                .withParams({ 'limit': 100 })
                .build();

            return server.rest(_.extend(baseRestRequestOptions, {url: url}))
                .then(_.identity); //only care about the first param (result), stops $.when passing an array of arguments to the handler
        }

        function getCurrentUser() {
            return server.rest(_.extend(baseRestRequestOptions, {url: baseRestUrl.addPathComponents('user').build()}))
                .then(_.identity);
        }

        return $.when(getOrganisations(), getCurrentUser()).done(function(orgResponse, userResponse) {
            changeStage(Stages.SELECT_REPOSITORY);
            var owners = [userResponse].concat(orgResponse.values);
            state.possibleOwners = owners;
            // by default the first item is selected so use it as the owner until changed.
            state.owner = owners[0];
            updateOwnerSelector(owners);
        });
    }

    function completionMessage(totalCount, successfulCount, failureCount, nameClashRepos, projectName) {
        return bitbucket.internal.importer.importCompleteMessaging({
            totalCount: totalCount,
            successfulCount: successfulCount,
            failureCount: failureCount,
            nameClashRepos: nameClashRepos,
            projectName: projectName
        });
    }

    function importComplete() {
        var totalCount = state.importing.tasks.length;
        var successfulCount = state.importing.tasks.filter(function(task) {
            return task.state === ImportState.SUCCESS;
        }).length;
        var failureCount = totalCount - successfulCount;
        var nameClashRepos = _.compact(state.importing.tasks.map(function(task) {
            if (task.failureType === FailureTypes.CREATE_DUPLICATE) {
                return task.externalRepoName;
            }
        }));

        var message = completionMessage(totalCount, successfulCount, failureCount, nameClashRepos, state.project.name);

        if (successfulCount === totalCount) {
            notifications.addFlash(null, {
                body: message,
                close: 'manual'
            });
        } else {
            notifications.addFlash(AJS.I18n.getText('bitbucket.importer.import.complete.failure.title'), {
                body: message,
                type: 'error',
                close: 'manual'
            });
        }

        window.location = navBuilder.project(state.project.key).build();
    }

    function pollForProgress() {
        var $bar = $('.progress-page .progress-bar .bar');
        var url = navBuilder.rest('importer', 'latest')
            .addPathComponents('projects', state.project.key, 'import', 'job', state.importing.jobId)
            .build();
        var completedCount = 0;
        var totalTasks = state.importing.tasks.length;

        server.poll({
            url: url,
            pollTimeout: importPollingTimeout,
            interval: 5000,
            tick: function(response) {
                state.importing.tasks = response.tasks;
                completedCount = state.importing.tasks.filter(function(task) {
                    return task.state === ImportState.FAILED || task.state === ImportState.SUCCESS;
                }).length;

                var completionProgress = ((completedCount / totalTasks) * 100) || 5;
                // always show a little bit of the polling bar so users know it's there
                $bar.width(completionProgress + '%');
                return completedCount === totalTasks || undefined;
            }
        });

        $bar.on(feature.transitionEndEventName(), function () {
            if (completedCount === totalTasks) {
                importComplete();
            }
        });
    }

    /**
     * When a repo list row selection change, update the list of selected repositories and re-render the buttons
     * @param {HTMLElement} rowInput
     */
    function repoRowChanged(rowInput) {
        if (rowInput.checked === true) {
            // add the item to the list
            var newRepo = rowInput.dataset;

            var repoExists = state.repos.some(function(repo) {
                return repo.name === newRepo.name && repo.url === newRepo.url;
            });

            if (!repoExists) {
                state.repos.push(newRepo);
            }
        } else {
            // remove the item from the list.
            state.repos = state.repos.filter(function(item) {
                return item.name !== rowInput.dataset.name;
            });
        }
        rerenderRepoListButtons();
    }

    /**
     * Update the owner selector with the given values.
     * @param {Array<Object>} values
     */
    function updateOwnerSelector(values) {
        // setup owner selector
        ownerSelector({
            selector: '#repo-owners',
            searchFieldThreshold: 25,
            data: {
                results: values,
                text: 'name'
            }
        }).on('change', function(e) {
            state.owner = _.find(state.possibleOwners, 'name', e.val);
            state.repos = [];
            if (state.repoSelectionMode === RepositorySelectionMode.SELECT) {
                updateRepositoryList();
            }
            rerenderRepoListButtons();
        });
    }

    /**
     * Update the repository list
     */
    function updateRepositoryList() {
        // only instantiate the repo list once per repo-selection screen lifecycle
        repoList = repoList || new RepositoryList('#repository-list', {
                urlBuilder: baseRestUrl.addPathComponents('repos'),
                rowChangeHandler: repoRowChanged
            });
        repoList.init({
            credential: state.credential,
            source: state.source,
            ownerName: state.owner.name
        });
    }

    /**
     * Push the active source-form input values to the state.
     */
    function pushActiveFormValuesToState() {
        $formBody.find('.source-form.active input').each(function(index, field) {
            _.set(state, field.parentNode.dataset.property, field.value);
        });
    }

    //
    // UI Event Handlers
    //

    /**
     * Changes providers
     */
    function importSourceButtonHandler() {
        if (this.dataset.source === state.source.type) {
            return;
        }
        resetLandingPageState(this.dataset.source);
        rerender();
    }

    /**
     * Check the url and credential fields for a source on key up (or focus out for the git clone url)
     */
    function sourceInputFieldHandler(event) {
        var oldFormValid = state.validForm;
        var property = this.parentNode.dataset.property;
        _.set(state, property, this.value);
        requiredFieldsPresent();

        if (oldFormValid !== state.validForm) {
            // rerenderConnect uses replaceWith which cleans up any events attached to the removed elements
            // in order to keep the "submit" click listen around, we need to only rerender when needed, not all the time
            rerenderConnect();
        }
    }

    /**
     * Switch out the github type to toggle between GH and GH:E
     */
    function githubTypeSwitchHandler() {
        var preGithubEnterprise = state.githubEnterprise;
        state.githubEnterprise = this.value === "true";

        if (state.githubEnterprise !== preGithubEnterprise) {
            state.source.url = '';
            rerenderGithub();
        }
    }

    /**
     * Connect to the source with credentials.
     * @param {Event} event
     */
    function validateGivenCredentials(event) {
        event.preventDefault();
        var button = this;
        var $spinnerButton = $(button).closest('.spinner-button');
        spinnerButtonToggle($spinnerButton, true);
        pushActiveFormValuesToState();

        if (state.source.type === Sources.GIT) {
            // import the given git repository
            importRepositories();
        } else {
            server.rest({
                url: baseRestUrl.addPathComponents('missing-oauth-scopes').build(),
                contextType: 'application/json',
                type: 'POST',
                data: {
                    credential: state.credential,
                    source: state.source
                }, statusCode: {
                    400: function (response, textStatus, errorThrown, errors, dominantError) {
                        var errorCode = false;
                        if (response && response.responseJSON && response.responseJSON.errors) {
                            errorCode = response.responseJSON.errors[0].context;
                            if (errorCode === ErrorStates.AUTHENTICATION) {
                                state.credential.error = AJS.I18n.getText('bitbucket.importer.import.form.error.credential.fail');
                            } else if (errorCode === ErrorStates.EXTERNAL_REQUEST) {
                                state.source.error = AJS.I18n.getText('bitbucket.importer.import.form.error.source');
                            }
                        }
                        state.validForm = false;
                        rerender();
                        return errorCode ? false : dominantError;
                    }
                }
            }).done(function (additionalPermissions) {
                if (additionalPermissions.length) {
                    state.credential.error = AJS.I18n.getText('bitbucket.importer.import.form.error.credential.morePermissions');
                    state.validForm = false;
                    rerender();
                } else {
                    if (state.source.type !== Sources.GIT) {
                        getOwner().always(function () {
                            spinnerButtonToggle($spinnerButton, false);
                        });
                    } else {
                        // import the given git repository
                        importRepositories();
                    }
                }
            });
        }
    }

    /**
     * Repository selection mode changed
     * @this {HTMLElement}
     */
    function repositorySelectionModeChanged() {
        state.repos = [];
        var repoListContainerEl = document.getElementById('repos');
        state.repoSelectionMode = this.parentElement.dataset.selectMode;
        repoListContainerEl.classList.toggle('hidden', state.repoSelectionMode === RepositorySelectionMode.ALL);
        if (state.repoSelectionMode === RepositorySelectionMode.SELECT) {
            updateRepositoryList();
        }
        rerenderRepoListButtons();
    }

    function submitRepos(event) {
        event.preventDefault();
        var button = this;
        var $spinnerButton = $(button).closest('.spinner-button');
        spinnerButtonToggle($spinnerButton, true);

        pushActiveFormValuesToState();
        importRepositories().always(function() {
            spinnerButtonToggle($spinnerButton, false);
        });
    }

    function importRepositories() {
        var specificEndpoint = state.repos.length > 0 ? 'repos' : 'owner-repos';
        var url = navBuilder.rest('importer', 'latest')
            .addPathComponents('projects', state.project.key, 'import', specificEndpoint)
            .build();

        var data = {
            source: state.source,
            externalRepositories: state.repos,
            owner: state.owner
        };

        if (state.credential.username) {
            data.credential = state.credential;
        }

        return server.rest({
            contextType: 'application/json',
            url: url,
            type: 'POST',
            data: data
        }).done(function(response) {
            state.importing.jobId = response.jobId;
            state.importing.tasks = response.tasks;

            analytics.add("importer.job.created", {
                "jobId"     : state.importing.jobId,
                "tasks"     : state.importing.tasks.length,
                "sourceType": state.source.type,
                "hasUrl"    : !(state.source.url === null)
            });

            changeStage(Stages.IMPORTING);
            pollForProgress();
        });
    }

    //
    // Initialisation
    //

    /**
     * Initialise the module
     * @param {Object} projectJSON
     */
    function init(projectJSON, fetchTimeout) {
        history.replaceState(null, null, "#" + state.stage);
        state.project.key = projectJSON.key;
        state.project.name = projectJSON.name;
        importPollingTimeout = fetchTimeout * 60 * 1000;
        $formBody = $('#import-form-body');
        $formBody.html(bitbucket.internal.importer.renderWithState({ state: state }));
        $formBody.on('click', '#import-source button', importSourceButtonHandler);
        $formBody.on('keyup focusout', '.connect-to-source .active .field-group:not(.git-url) input', sourceInputFieldHandler);
        $formBody.on('focusout', '.connect-to-source .active .git-url input', sourceInputFieldHandler);
        $formBody.on('click', '.github-specific input.radio', githubTypeSwitchHandler);
        $formBody.on('click', '.connect-to-source input[type=submit]', validateGivenCredentials);
        $formBody.on('change', '#repositories input', repositorySelectionModeChanged);
        $formBody.on('click', '.select-repositories input[type=submit]', submitRepos);
        $formBody.on('click', 'button.cancel', function (e) {
            e.preventDefault();
            window.history.back();
        });
        history.on('popstate', function() {
            state = $.extend(true, state, {
                credential: {
                    username: null,
                    password: null,
                    error: null },
                validForm: false
            });
            changeStage(window.location.hash.split('#')[1], true);
        });
        analytics.add("importer.form.initialised", {"sourceType": state.source.type});
    }

    /**
     * @param {Object} projectJSON
     * @param {Number} fetchTimeout - how longer the server will fetch repos for before timing out, in minutes
     */
    function onReady(projectJSON, fetchTimeout) {
        $(document).ready(function() {
            init(projectJSON, fetchTimeout);
        });
    }

    exports.onReady = onReady;
    exports._completionMessage = completionMessage; // for testing
});
