/*
 * Copyright (C) 2019 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.
 */

#pragma once

#include <android/content/pm/BnDataLoaderStatusListener.h>
#include <android/content/pm/DataLoaderParamsParcel.h>
#include <android/content/pm/FileSystemControlParcel.h>
#include <android/content/pm/IDataLoaderStatusListener.h>
#include <android/os/incremental/BnIncrementalService.h>
#include <android/os/incremental/BnIncrementalServiceConnector.h>
#include <android/os/incremental/BnStorageHealthListener.h>
#include <android/os/incremental/BnStorageLoadingProgressListener.h>
#include <android/os/incremental/PerUidReadTimeouts.h>
#include <android/os/incremental/StorageHealthCheckParams.h>
#include <binder/IAppOpsCallback.h>
#include <binder/PersistableBundle.h>
#include <utils/String16.h>
#include <utils/StrongPointer.h>
#include <ziparchive/zip_archive.h>

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <limits>
#include <map>
#include <mutex>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "ServiceWrappers.h"
#include "incfs.h"
#include "path.h"

namespace android::incremental {

using MountId = int;
using StorageId = int;
using FileId = incfs::FileId;
using BlockIndex = incfs::BlockIndex;
using RawMetadata = incfs::RawMetadata;
using Seconds = std::chrono::seconds;
using BootClockTsUs = uint64_t;

using IDataLoaderStatusListener = ::android::content::pm::IDataLoaderStatusListener;
using DataLoaderStatusListener = ::android::sp<IDataLoaderStatusListener>;

using StorageHealthCheckParams = ::android::os::incremental::StorageHealthCheckParams;
using IStorageHealthListener = ::android::os::incremental::IStorageHealthListener;
using StorageHealthListener = ::android::sp<IStorageHealthListener>;
using IStorageLoadingProgressListener = ::android::os::incremental::IStorageLoadingProgressListener;
using StorageLoadingProgressListener = ::android::sp<IStorageLoadingProgressListener>;

using PerUidReadTimeouts = ::android::os::incremental::PerUidReadTimeouts;

struct IfsState {
    // If mount is fully loaded.
    bool fullyLoaded = false;
    // If read logs are enabled on this mount. Populated only if fullyLoaded == true.
    bool readLogsEnabled = false;
    // If there was an error fetching any of the above.
    bool error = false;
};
// Returns true if wants to be called again.
using IfsStateCallback = std::function<bool(StorageId, IfsState)>;

class IncrementalService final {
public:
    explicit IncrementalService(ServiceManagerWrapper&& sm, std::string_view rootDir);

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
    ~IncrementalService();
#pragma GCC diagnostic pop

    static constexpr StorageId kInvalidStorageId = -1;
    static constexpr StorageId kMaxStorageId = std::numeric_limits<int>::max() - 1;
    static constexpr StorageId kAllStoragesId = kMaxStorageId + 1;

    static constexpr BootClockTsUs kMaxBootClockTsUs = std::numeric_limits<BootClockTsUs>::max();

    enum CreateOptions {
        TemporaryBind = 1,
        PermanentBind = 2,
        CreateNew = 4,
        OpenExisting = 8,

        Default = TemporaryBind | CreateNew
    };

    enum class BindKind {
        Temporary = 0,
        Permanent = 1,
    };

    enum StorageFlags {
        ReadLogsAllowed = 1 << 0,
        ReadLogsEnabled = 1 << 1,
        ReadLogsRequested = 1 << 2,

        ReadTimeoutsEnabled = 1 << 3,
        ReadTimeoutsRequested = 1 << 4,
    };

    struct LoadingProgress {
        ssize_t filledBlocks;
        ssize_t totalBlocks;

        bool isError() const { return totalBlocks < 0; }
        bool started() const { return totalBlocks > 0; }
        bool fullyLoaded() const { return !isError() && (totalBlocks == filledBlocks); }

        int blocksRemainingOrError() const {
            return totalBlocks <= 0 ? totalBlocks : totalBlocks - filledBlocks;
        }

        float getProgress() const {
            return totalBlocks < 0
                    ? totalBlocks
                    : totalBlocks > 0 ? double(filledBlocks) / double(totalBlocks) : 1.f;
        }
    };

    static FileId idFromMetadata(std::span<const uint8_t> metadata);
    static inline FileId idFromMetadata(std::span<const char> metadata) {
        return idFromMetadata({(const uint8_t*)metadata.data(), metadata.size()});
    }

    void onDump(int fd);

    void onSystemReady();

    StorageId createStorage(std::string_view mountPoint,
                            content::pm::DataLoaderParamsParcel dataLoaderParams,
                            CreateOptions options);
    StorageId createLinkedStorage(std::string_view mountPoint, StorageId linkedStorage,
                                  CreateOptions options = CreateOptions::Default);
    StorageId openStorage(std::string_view path);

    bool startLoading(StorageId storage, content::pm::DataLoaderParamsParcel dataLoaderParams,
                      DataLoaderStatusListener statusListener,
                      const StorageHealthCheckParams& healthCheckParams,
                      StorageHealthListener healthListener,
                      std::vector<PerUidReadTimeouts> perUidReadTimeouts);
    void onInstallationComplete(StorageId storage);

    int bind(StorageId storage, std::string_view source, std::string_view target, BindKind kind);
    int unbind(StorageId storage, std::string_view target);
    void deleteStorage(StorageId storage);

    void disallowReadLogs(StorageId storage);
    int setStorageParams(StorageId storage, bool enableReadLogs);

    int makeFile(StorageId storage, std::string_view path, int mode, FileId id,
                 incfs::NewFileParams params, std::span<const uint8_t> data);
    int makeDir(StorageId storage, std::string_view path, int mode = 0755);
    int makeDirs(StorageId storage, std::string_view path, int mode = 0755);

    int link(StorageId sourceStorageId, std::string_view oldPath, StorageId destStorageId,
             std::string_view newPath);
    int unlink(StorageId storage, std::string_view path);

    incfs::LoadingState isFileFullyLoaded(StorageId storage, std::string_view filePath) const;
    incfs::LoadingState isMountFullyLoaded(StorageId storage) const;

    LoadingProgress getLoadingProgress(StorageId storage) const;

    bool registerLoadingProgressListener(StorageId storage,
                                         StorageLoadingProgressListener progressListener);
    bool unregisterLoadingProgressListener(StorageId storage);

    RawMetadata getMetadata(StorageId storage, std::string_view path) const;
    RawMetadata getMetadata(StorageId storage, FileId node) const;

    bool configureNativeBinaries(StorageId storage, std::string_view apkFullPath,
                                 std::string_view libDirRelativePath, std::string_view abi,
                                 bool extractNativeLibs);
    bool waitForNativeBinariesExtraction(StorageId storage);

    void getMetrics(int32_t storageId, android::os::PersistableBundle* _aidl_return);

    class AppOpsListener : public android::BnAppOpsCallback {
    public:
        AppOpsListener(IncrementalService& incrementalService, std::string packageName)
              : incrementalService(incrementalService), packageName(std::move(packageName)) {}
        void opChanged(int32_t op, const String16& packageName) final;

    private:
        IncrementalService& incrementalService;
        const std::string packageName;
    };

    class IncrementalServiceConnector : public os::incremental::BnIncrementalServiceConnector {
    public:
        IncrementalServiceConnector(IncrementalService& incrementalService, int32_t storage)
              : incrementalService(incrementalService), storage(storage) {}
        binder::Status setStorageParams(bool enableReadLogs, int32_t* _aidl_return) final;

    private:
        IncrementalService& incrementalService;
        int32_t const storage;
    };

private:
    struct IncFsMount;

    class DataLoaderStub : public content::pm::BnDataLoaderStatusListener {
    public:
        DataLoaderStub(IncrementalService& service, MountId id,
                       content::pm::DataLoaderParamsParcel&& params,
                       content::pm::FileSystemControlParcel&& control,
                       DataLoaderStatusListener&& statusListener,
                       const StorageHealthCheckParams& healthCheckParams,
                       StorageHealthListener&& healthListener, std::string&& healthPath);
        ~DataLoaderStub();
        // Cleans up the internal state and invalidates DataLoaderStub. Any subsequent calls will
        // result in an error.
        void cleanupResources();

        bool requestCreate();
        bool requestStart();
        bool requestDestroy();

        void onDump(int fd);

        MountId id() const { return mId.load(std::memory_order_relaxed); }
        const content::pm::DataLoaderParamsParcel& params() const { return mParams; }
        bool isSystemDataLoader() const;
        void setHealthListener(const StorageHealthCheckParams& healthCheckParams,
                               StorageHealthListener&& healthListener);
        void getMetrics(android::os::PersistableBundle* _aidl_return);

    private:
        binder::Status onStatusChanged(MountId mount, int newStatus) final;

        void setCurrentStatus(int newStatus);
        void compareAndSetCurrentStatus(int expectedStatus, int newStatus);

        sp<content::pm::IDataLoader> getDataLoader();

        bool bind();
        bool create();
        bool start();
        bool destroy();

        bool setTargetStatus(int status);
        void setTargetStatusLocked(int status);

        bool fsmStep();

        void onHealthStatus(const StorageHealthListener& healthListener, int healthStatus);
        void updateHealthStatus(bool baseline = false);

        bool isValid() const { return id() != kInvalidStorageId; }

        bool isHealthParamsValid() const;

        const incfs::UniqueControl& initializeHealthControl();
        void resetHealthControl();

        BootClockTsUs getOldestPendingReadTs();
        BootClockTsUs getOldestTsFromLastPendingReads();
        Milliseconds elapsedMsSinceKernelTs(TimePoint now, BootClockTsUs kernelTsUs);
        long elapsedMsSinceOldestPendingRead();

        // If the stub has to bind to the DL.
        // Returns {} if bind operation is already in progress.
        // Or bind delay in ms.
        std::optional<Milliseconds> needToBind();

        void registerForPendingReads();
        void unregisterFromPendingReads();

        IncrementalService& mService;

        std::mutex mMutex;
        std::atomic<MountId> mId = kInvalidStorageId;
        content::pm::DataLoaderParamsParcel mParams;
        content::pm::FileSystemControlParcel mControl;
        DataLoaderStatusListener mStatusListener;
        StorageHealthListener mHealthListener;
        std::atomic<int> mHealthStatus = IStorageHealthListener::HEALTH_STATUS_OK;

        std::condition_variable mStatusCondition;
        int mCurrentStatus = content::pm::IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
        TimePoint mCurrentStatusTs = {};
        int mTargetStatus = content::pm::IDataLoaderStatusListener::DATA_LOADER_DESTROYED;
        TimePoint mTargetStatusTs = {};

        TimePoint mPreviousBindTs = {};
        Milliseconds mPreviousBindDelay = {};

        std::string mHealthPath;
        incfs::UniqueControl mHealthControl;
        struct {
            TimePoint userTs;
            BootClockTsUs kernelTsUs;
        } mHealthBase = {TimePoint::max(), kMaxBootClockTsUs};
        StorageHealthCheckParams mHealthCheckParams;
        std::vector<incfs::ReadInfoWithUid> mLastPendingReads;
    };
    using DataLoaderStubPtr = sp<DataLoaderStub>;

    struct IncFsMount {
        struct Bind {
            StorageId storage;
            std::string savedFilename;
            std::string sourceDir;
            BindKind kind;
        };

        struct Storage {
            std::string name;
        };

        using Control = incfs::UniqueControl;

        using BindMap = std::map<std::string, Bind, path::PathLess>;
        using StorageMap = std::unordered_map<StorageId, Storage>;

        mutable std::mutex lock;
        const std::string root;
        const std::string metricsKey;
        Control control;
        /*const*/ MountId mountId;
        int32_t flags = StorageFlags::ReadLogsAllowed;
        StorageMap storages;
        BindMap bindPoints;
        DataLoaderStubPtr dataLoaderStub;
        TimePoint startLoadingTs = {};
        std::atomic<int> nextStorageDirNo{0};
        const IncrementalService& incrementalService;

        IncFsMount(std::string root, std::string metricsKey, MountId mountId, Control control,
                   const IncrementalService& incrementalService)
              : root(std::move(root)),
                metricsKey(std::move(metricsKey)),
                control(std::move(control)),
                mountId(mountId),
                incrementalService(incrementalService) {}
        IncFsMount(IncFsMount&&) = delete;
        IncFsMount& operator=(IncFsMount&&) = delete;
        ~IncFsMount();

        StorageMap::iterator makeStorage(StorageId id);

        void disallowReadLogs() { flags &= ~StorageFlags::ReadLogsAllowed; }
        int32_t readLogsAllowed() const { return (flags & StorageFlags::ReadLogsAllowed); }

        void setReadLogsEnabled(bool value) {
            return setFlag(StorageFlags::ReadLogsEnabled, value);
        }
        int32_t readLogsEnabled() const { return (flags & StorageFlags::ReadLogsEnabled); }

        void setReadLogsRequested(bool value) {
            return setFlag(StorageFlags::ReadLogsRequested, value);
        }
        int32_t readLogsRequested() const { return (flags & StorageFlags::ReadLogsRequested); }

        void setReadTimeoutsEnabled(bool value) {
            return setFlag(StorageFlags::ReadTimeoutsEnabled, value);
        }
        int32_t readTimeoutsEnabled() const { return (flags & StorageFlags::ReadTimeoutsEnabled); }

        void setReadTimeoutsRequested(bool value) {
            return setFlag(StorageFlags::ReadTimeoutsRequested, value);
        }
        int32_t readTimeoutsRequested() const {
            return (flags & StorageFlags::ReadTimeoutsRequested);
        }

        static void cleanupFilesystem(std::string_view root);

    private:
        void setFlag(StorageFlags flag, bool value);
    };

    using IfsMountPtr = std::shared_ptr<IncFsMount>;
    using MountMap = std::unordered_map<MountId, IfsMountPtr>;
    using BindPathMap = std::map<std::string, IncFsMount::BindMap::iterator, path::PathLess>;

    static bool perfLoggingEnabled();

    void setUidReadTimeouts(StorageId storage,
                            std::vector<PerUidReadTimeouts>&& perUidReadTimeouts);
    void clearUidReadTimeouts(StorageId storage);
    bool checkUidReadTimeouts(StorageId storage, IfsState state, Clock::time_point timeLimit);

    std::unordered_set<std::string_view> adoptMountedInstances();
    void mountExistingImages(const std::unordered_set<std::string_view>& mountedRootNames);
    bool mountExistingImage(std::string_view root);

    IfsMountPtr getIfs(StorageId storage) const;
    const IfsMountPtr& getIfsLocked(StorageId storage) const;
    int addBindMount(IncFsMount& ifs, StorageId storage, std::string_view storageRoot,
                     std::string&& source, std::string&& target, BindKind kind,
                     std::unique_lock<std::mutex>& mainLock);

    int addBindMountWithMd(IncFsMount& ifs, StorageId storage, std::string&& metadataName,
                           std::string&& source, std::string&& target, BindKind kind,
                           std::unique_lock<std::mutex>& mainLock);

    void addBindMountRecordLocked(IncFsMount& ifs, StorageId storage, std::string&& metadataName,
                                  std::string&& source, std::string&& target, BindKind kind);

    bool needStartDataLoaderLocked(IncFsMount& ifs);

    void prepareDataLoaderLocked(IncFsMount& ifs, content::pm::DataLoaderParamsParcel&& params,
                                 DataLoaderStatusListener&& statusListener = {},
                                 const StorageHealthCheckParams& healthCheckParams = {},
                                 StorageHealthListener&& healthListener = {});

    BindPathMap::const_iterator findStorageLocked(std::string_view path) const;
    StorageId findStorageId(std::string_view path) const;

    void deleteStorage(IncFsMount& ifs);
    void deleteStorageLocked(IncFsMount& ifs, std::unique_lock<std::mutex>&& ifsLock);
    MountMap::iterator getStorageSlotLocked();
    std::string normalizePathToStorage(const IncFsMount& incfs, StorageId storage,
                                       std::string_view path) const;
    std::string normalizePathToStorageLocked(const IncFsMount& incfs,
                                             IncFsMount::StorageMap::const_iterator storageIt,
                                             std::string_view path) const;
    int makeDirs(const IncFsMount& ifs, StorageId storageId, std::string_view path, int mode);

    int disableReadLogsLocked(IncFsMount& ifs);
    int applyStorageParamsLocked(IncFsMount& ifs);

    LoadingProgress getLoadingProgressFromPath(const IncFsMount& ifs, std::string_view path) const;

    int setFileContent(const IfsMountPtr& ifs, const incfs::FileId& fileId,
                       std::string_view debugFilePath, std::span<const uint8_t> data) const;

    void registerAppOpsCallback(const std::string& packageName);
    bool unregisterAppOpsCallback(const std::string& packageName);
    void onAppOpChanged(const std::string& packageName);

    void runJobProcessing();
    void extractZipFile(const IfsMountPtr& ifs, ZipArchiveHandle zipFile, ZipEntry& entry,
                        const incfs::FileId& libFileId, std::string_view debugLibPath,
                        Clock::time_point scheduledTs);

    void runCmdLooper();

    bool addTimedJob(TimedQueueWrapper& timedQueue, MountId id, Milliseconds after, Job what);
    bool removeTimedJobs(TimedQueueWrapper& timedQueue, MountId id);

    void addIfsStateCallback(StorageId storageId, IfsStateCallback callback);
    void removeIfsStateCallbacks(StorageId storageId);
    void processIfsStateCallbacks();
    void processIfsStateCallbacks(StorageId storageId, std::vector<IfsStateCallback>& callbacks);

    bool updateLoadingProgress(int32_t storageId,
                               StorageLoadingProgressListener&& progressListener);

    void trimReservedSpaceV1(const IncFsMount& ifs);
    int64_t elapsedUsSinceMonoTs(uint64_t monoTsUs);

private:
    const std::unique_ptr<VoldServiceWrapper> mVold;
    const std::unique_ptr<DataLoaderManagerWrapper> mDataLoaderManager;
    const std::unique_ptr<IncFsWrapper> mIncFs;
    const std::unique_ptr<AppOpsManagerWrapper> mAppOpsManager;
    const std::unique_ptr<JniWrapper> mJni;
    const std::unique_ptr<LooperWrapper> mLooper;
    const std::unique_ptr<TimedQueueWrapper> mTimedQueue;
    const std::unique_ptr<TimedQueueWrapper> mProgressUpdateJobQueue;
    const std::unique_ptr<FsWrapper> mFs;
    const std::unique_ptr<ClockWrapper> mClock;
    const std::string mIncrementalDir;

    mutable std::mutex mLock;
    mutable std::mutex mMountOperationLock;
    MountMap mMounts;
    BindPathMap mBindsByPath;

    std::mutex mCallbacksLock;
    std::unordered_map<std::string, sp<AppOpsListener>> mCallbackRegistered;

    using IfsStateCallbacks = std::map<StorageId, std::vector<IfsStateCallback>>;
    std::mutex mIfsStateCallbacksLock;
    IfsStateCallbacks mIfsStateCallbacks;

    std::atomic_bool mSystemReady = false;
    StorageId mNextId = 0;

    std::atomic_bool mRunning{true};

    std::unordered_map<MountId, std::vector<Job>> mJobQueue;
    MountId mPendingJobsMount = kInvalidStorageId;
    std::condition_variable mJobCondition;
    std::mutex mJobMutex;
    std::thread mJobProcessor;

    std::thread mCmdLooperThread;
};

} // namespace android::incremental
