// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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.

import { Injectable } from '@angular/core';
import { CoreSyncBlockedError } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreApp } from '@services/app';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModFeedback, AddonModFeedbackProvider, AddonModFeedbackWSFeedback } from './feedback';
import { AddonModFeedbackOffline, AddonModFeedbackOfflineResponse } from './feedback-offline';
import { AddonModFeedbackPrefetchHandler, AddonModFeedbackPrefetchHandlerService } from './handlers/prefetch';

/**
 * Service to sync feedbacks.
 */
@Injectable({ providedIn: 'root' })
export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModFeedbackSyncResult> {

    static readonly AUTO_SYNCED = 'addon_mod_feedback_autom_synced';

    protected componentTranslatableString = 'feedback';

    constructor() {
        super('AddonModFeedbackSyncProvider');
    }

    /**
     * @inheritdoc
     */
    prefetchAfterUpdate(
        prefetchHandler: AddonModFeedbackPrefetchHandlerService,
        module: CoreCourseAnyModuleData,
        courseId: number,
        regex?: RegExp,
        siteId?: string,
    ): Promise<void> {
        regex = regex || /^.*files$|^timers/;

        return super.prefetchAfterUpdate(prefetchHandler, module, courseId, regex, siteId);
    }

    /**
     * Try to synchronize all the feedbacks in a certain site or in all sites.
     *
     * @param siteId Site ID to sync. If not defined, sync all sites.
     * @param force Wether to force sync not depending on last execution.
     * @return Promise resolved if sync is successful, rejected if sync fails.
     */
    syncAllFeedbacks(siteId?: string, force?: boolean): Promise<void> {
        return this.syncOnSites('all feedbacks', this.syncAllFeedbacksFunc.bind(this, !!force), siteId);
    }

    /**
     * Sync all pending feedbacks on a site.
     *
     * @param force Wether to force sync not depending on last execution.
     * @param siteId Site ID to sync. If not defined, sync all sites.
     * @param Promise resolved if sync is successful, rejected if sync fails.
     */
    protected async syncAllFeedbacksFunc(force: boolean, siteId?: string): Promise<void> {
        // Sync all new responses.
        const responses = await AddonModFeedbackOffline.getAllFeedbackResponses(siteId);

        // Do not sync same feedback twice.
        const treated: Record<number, boolean> = {};

        await Promise.all(responses.map(async (response) => {
            if (treated[response.feedbackid]) {
                return;
            }

            treated[response.feedbackid] = true;

            const result = force ?
                await this.syncFeedback(response.feedbackid, siteId) :
                await this.syncFeedbackIfNeeded(response.feedbackid, siteId);

            if (result?.updated) {
                // Sync successful, send event.
                CoreEvents.trigger(AddonModFeedbackSyncProvider.AUTO_SYNCED, {
                    feedbackId: response.feedbackid,
                    warnings: result.warnings,
                }, siteId);
            }
        }));
    }

    /**
     * Sync a feedback only if a certain time has passed since the last time.
     *
     * @param feedbackId Feedback ID.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved when the feedback is synced or if it doesn't need to be synced.
     */
    async syncFeedbackIfNeeded(feedbackId: number, siteId?: string): Promise<AddonModFeedbackSyncResult | undefined> {
        siteId = siteId || CoreSites.getCurrentSiteId();

        const needed = await this.isSyncNeeded(feedbackId, siteId);

        if (needed) {
            return this.syncFeedback(feedbackId, siteId);
        }
    }

    /**
     * Synchronize all offline responses of a feedback.
     *
     * @param feedbackId Feedback ID to be synced.
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved if sync is successful, rejected otherwise.
     */
    syncFeedback(feedbackId: number, siteId?: string): Promise<AddonModFeedbackSyncResult> {
        siteId = siteId || CoreSites.getCurrentSiteId();

        if (this.isSyncing(feedbackId, siteId)) {
            // There's already a sync ongoing for this feedback, return the promise.
            return this.getOngoingSync(feedbackId, siteId)!;
        }

        // Verify that feedback isn't blocked.
        if (CoreSync.isBlocked(AddonModFeedbackProvider.COMPONENT, feedbackId, siteId)) {
            this.logger.debug(`Cannot sync feedback '${feedbackId}' because it is blocked.`);

            throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
        }

        this.logger.debug(`Try to sync feedback '${feedbackId}' in site ${siteId}'`);

        return this.addOngoingSync(feedbackId, this.performSyncFeedback(feedbackId, siteId), siteId);
    }

    /**
     * Perform the feedback sync.
     *
     * @param feedbackId Feedback ID.
     * @param siteId Site ID.
     * @return Promise resolved in success.
     */
    protected async performSyncFeedback(feedbackId: number, siteId: string): Promise<AddonModFeedbackSyncResult> {
        const result: AddonModFeedbackSyncResult = {
            warnings: [],
            updated: false,
        };

        // Sync offline logs.
        await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModFeedbackProvider.COMPONENT, feedbackId, siteId));

        // Get offline responses to be sent.
        const responses = await CoreUtils.ignoreErrors(AddonModFeedbackOffline.getFeedbackResponses(feedbackId, siteId));

        if (!responses || !responses.length) {
            // Nothing to sync.
            await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId));

            return result;
        }

        if (!CoreApp.isOnline()) {
            // Cannot sync in offline.
            throw new CoreNetworkError();
        }

        const courseId = responses[0].courseid;

        const feedback = await AddonModFeedback.getFeedbackById(courseId, feedbackId, { siteId });

        if (!feedback.multiple_submit) {
            // If it does not admit multiple submits, check if it is completed to know if we can submit.
            const isCompleted = await AddonModFeedback.isCompleted(feedbackId, { cmId: feedback.coursemodule, siteId });

            if (isCompleted) {
                // Cannot submit again, delete resposes.
                await Promise.all(responses.map((data) =>
                    AddonModFeedbackOffline.deleteFeedbackPageResponses(feedbackId, data.page, siteId)));

                result.updated = true;
                this.addOfflineDataDeletedWarning(
                    result.warnings,
                    feedback.name,
                    Translate.instant('addon.mod_feedback.this_feedback_is_already_submitted'),
                );

                await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId));

                return result;
            }
        }

        const timemodified = await AddonModFeedback.getCurrentCompletedTimeModified(feedbackId, {
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
            siteId,
        });
        // Sort by page.
        responses.sort((a, b) => a.page - b.page);

        const orderedData = responses.map((data) => ({
            function: this.processPage.bind(this, feedback, data, siteId, timemodified, result),
            blocking: true,
        }));

        // Execute all the processes in order to solve dependencies.
        await CoreUtils.executeOrderedPromises(orderedData);

        if (result.updated) {
            // Data has been sent to server, update data.
            try {
                const module = await CoreCourse.getModuleBasicInfoByInstance(feedbackId, 'feedback', siteId);

                await this.prefetchAfterUpdate(AddonModFeedbackPrefetchHandler.instance, module, courseId, undefined, siteId);
            } catch {
                // Ignore errors.
            }
        }

        // Sync finished, set sync time.
        await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId));

        return result;
    }

    /**
     * Convenience function to sync process page calls.
     *
     * @param feedback Feedback object.
     * @param data Response data.
     * @param siteId Site Id.
     * @param timemodified Current completed modification time.
     * @param result Result object to be modified.
     * @return Resolve when done or rejected with error.
     */
    protected async processPage(
        feedback: AddonModFeedbackWSFeedback,
        data: AddonModFeedbackOfflineResponse,
        siteId: string,
        timemodified: number,
        result: AddonModFeedbackSyncResult,
    ): Promise<void> {
        // Delete all pages that are submitted before changing website.
        if (timemodified > data.timemodified) {
            return AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId);
        }

        try {
            await AddonModFeedback.processPageOnline(feedback.id, data.page, data.responses, false, siteId);

            result.updated = true;

            await AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId);
        } catch (error) {
            if (!CoreUtils.isWebServiceError(error)) {
                // Couldn't connect to server, reject.
                throw error;
            }

            // The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
            result.updated = true;

            await AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId);

            // Responses deleted, add a warning.
            this.addOfflineDataDeletedWarning(
                result.warnings,
                feedback.name,
                error,
            );
        }
    }

}

export const AddonModFeedbackSync = makeSingleton(AddonModFeedbackSyncProvider);

/**
 * Data returned by a feedback sync.
 */
export type AddonModFeedbackSyncResult = {
    warnings: string[]; // List of warnings.
    updated: boolean; // Whether some data was sent to the server or offline data was updated.
};

/**
 * Data passed to AUTO_SYNCED event.
 */
export type AddonModFeedbackAutoSyncData = {
    feedbackId: number;
    warnings: string[];
};
