/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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.android.providers.media;

import static android.os.Process.THREAD_PRIORITY_FOREGROUND;

import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAllAvailableCloudProviders;
import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders;

import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.HandlerExecutor;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.Executor;

class MediaProviderShellCommand extends BasicShellCommandHandler {
    private final @NonNull Context mAppContext;
    private final @NonNull ConfigStore mConfigStore;
    private final @NonNull PickerSyncController mPickerSyncController;
    private final @NonNull OutputStream mOut;

    MediaProviderShellCommand(
            @NonNull Context context,
            @NonNull ConfigStore configStore,
            @NonNull PickerSyncController pickerSyncController,
            @NonNull ParcelFileDescriptor out) {
        mAppContext = context.getApplicationContext();
        mPickerSyncController = pickerSyncController;
        mConfigStore = configStore;
        mOut = new ParcelFileDescriptor.AutoCloseOutputStream(out);
    }

    @Override
    public int onCommand(String cmd) {
        try (PrintWriter pw = getOutPrintWriter()) {
            if (cmd == null || cmd.isBlank()) {
                cmd = "help";
            }
            switch (cmd) {
                case "version":
                    return runVersion(pw);
                case "cloud-provider":
                    return runCloudProvider(pw);
                default:
                    return handleDefaultCommands(cmd);
            }
        }
    }

    private int runVersion(@NonNull PrintWriter pw) {
        pw.print('\'' + DatabaseHelper.INTERNAL_DATABASE_NAME + "' version: ");
        pw.println(DatabaseHelper.VERSION_LATEST);

        pw.print('\'' + DatabaseHelper.EXTERNAL_DATABASE_NAME + "' version: ");
        pw.println(DatabaseHelper.VERSION_LATEST);

        pw.print('\'' + PickerDatabaseHelper.PICKER_DATABASE_NAME + "' version: ");
        pw.println(PickerDatabaseHelper.VERSION_LATEST);

        return 0;
    }

    private int runCloudProvider(@NonNull PrintWriter pw) {
        final String subcommand = getNextArgRequired();
        switch (subcommand) {
            case "list":
                return runCloudProviderList(pw);
            case "info":
                return runCloudProviderInfo(pw);
            case "set":
                return runCloudProviderSet(pw);
            case "unset":
                return runCloudProviderUnset(pw);
            case "sync-library":
                return runCloudProviderSyncLibrary(pw);
            case "reset-library":
                return runCloudProviderResetLibrary(pw);
            default:
                pw.println("Error: unknown cloud-provider command '" + subcommand + "'");
                return 1;
        }
    }

    private int runCloudProviderList(@NonNull PrintWriter pw) {
        final String option = getNextOption();
        if ("--allowlist".equals(option)) {
            final List<String> allowlist = mConfigStore.getAllowedCloudProviderPackages();
            if (allowlist.isEmpty()) {
                pw.println("Allowlist is empty.");
            } else {
                for (var providerAuthority : allowlist) {
                    pw.println(providerAuthority);
                }
            }
        } else {
            final List<CloudProviderInfo> cloudProviders;

            if ("--all".equals(option)) {
                cloudProviders = getAllAvailableCloudProviders(mAppContext, mConfigStore);
            } else if (option == null) {
                cloudProviders = getAvailableCloudProviders(mAppContext, mConfigStore);
            } else {
                pw.println("Error: unknown cloud-provider list option '" + option + "'");
                return 1;
            }

            if (cloudProviders.isEmpty()) {
                pw.println("No available CloudMediaProviders.");
            } else {
                for (var providerInfo : cloudProviders) {
                    pw.println(providerInfo.toShortString());
                }
            }
        }
        return 0;
    }

    private int runCloudProviderInfo(@NonNull PrintWriter pw) {
        pw.println("Current CloudMediaProvider:");
        pw.println(mPickerSyncController.getCurrentCloudProviderInfo().toShortString());
        return 0;
    }

    private int runCloudProviderSet(@NonNull PrintWriter pw) {
        final String authority = getNextArg();
        if (authority == null) {
            pw.println("Error: authority not provided");
            pw.println("(usage: `media_provider cloud-provider set <authority>`)");
            return 1;
        }

        pw.println("Setting current CloudMediaProvider authority to '" + authority + "'...");
        final boolean success = mPickerSyncController.forceSetCloudProvider(authority);

        pw.println(success ?  "Succeed." : "Failed.");
        return success ? 0 : 1;
    }

    private int runCloudProviderUnset(@NonNull PrintWriter pw) {
        pw.println("Unsetting current CloudMediaProvider (disabling CMP integration)...");
        final boolean success = mPickerSyncController.forceSetCloudProvider(null);

        pw.println(success ?  "Succeed." : "Failed.");
        return success ? 0 : 1;
    }

    private int runCloudProviderSyncLibrary(@NonNull PrintWriter pw) {
        pw.println("Syncing PhotoPicker's library (CMP and local)...");

        // TODO(b/242550131): add PickerSyncController's API to make it possible to sync from only
        //  one provider at a time (i.e. either CMP or local)
        mPickerSyncController.syncAllMedia();

        pw.println("Done.");
        return 0;
    }

    private int runCloudProviderResetLibrary(@NonNull PrintWriter pw) {
        pw.println("Resetting PhotoPicker's library (CMP and local)...");

        // TODO(b/242550131): add PickerSyncController's API to make it possible to reset just one
        //  provider's library at a time (i.e. either CMP or local).
        try {
            mPickerSyncController.resetAllMedia();
        } catch (UnableToAcquireLockException e) {
            pw.print("Could not reset all media" + e.getMessage());
            return 1;
        }

        pw.println("Done.");
        return 0;
    }

    @Override
    public void onHelp() {
        final PrintWriter pw = getOutPrintWriter();
        pw.println("MediaProvider (media_provider) commands:");
        pw.println("  help");
        pw.println("      Print this help text.");
        pw.println();
        pw.println("  version");
        pw.println("      Print databases (internal/external/picker) versions.");
        pw.println();
        pw.println("  cloud-provider [list | info | set | unset] [...]");
        pw.println("      Configure and audit CloudMediaProvider-s (CMPs).");
        pw.println();
        pw.println("      list  [--all | --allowlist]");
        pw.println("          List installed and allowlisted CMPs.");
        pw.println("          --all: ignore allowlist, list all installed CMPs.");
        pw.println("          --allowlisted: print allowlist of CMP authorities.");
        pw.println();
        pw.println("      info");
        pw.println("          Print current CloudMediaProvider.");
        pw.println();
        pw.println("      set <AUTHORITY>");
        pw.println("          Set current CloudMediaProvider.");
        pw.println();
        pw.println("      unset");
        pw.println("          Unset CloudMediaProvider (disables CMP integration).");
        pw.println();
        pw.println("      sync-library");
        pw.println("          Sync media from the current CloudMediaProvider and local provider.");
        pw.println();
        pw.println("      reset-library");
        pw.println("          Reset media previously synced from the CloudMediaProvider and");
        pw.println("          the local provider.");
        pw.println();
    }

    public void exec(@Nullable String[] args) {
        getExecutor().execute(() -> exec(
                /* Binder target */ null,
                /* FileDescriptor in */ null,
                /* FileDescriptor out */ null,
                /* FileDescriptor err */ null,
                args));
    }


    @Override
    public OutputStream getRawOutputStream() {
        return mOut;
    }

    @Override
    public OutputStream getRawErrorStream() {
        return mOut;
    }

    @Nullable
    private static Executor sExecutor;

    @NonNull
    private static synchronized Executor getExecutor() {
        if (sExecutor == null) {
            final HandlerThread thread = new HandlerThread("cli", THREAD_PRIORITY_FOREGROUND);
            thread.start();
            final Handler handler = new Handler(thread.getLooper());
            sExecutor = new HandlerExecutor(handler);
        }
        return sExecutor;
    }
}
