/*
 * Copyright (C) 2025 Huawei Device Co., 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.
 */
#include <ani.h>
#include <fcntl.h>
#include <securec.h>
#include <sys/stat.h>
#include <filesystem>
#include <iostream>
#include <regex>
#include <string>
#include <system_error>

#include "constant.h"
#include "class.h"
#include "log.h"
#include "memory.h"
#include "ani_utils.h"
#include "ani_task.h"
#include "ani_js_initialize.h"
#include "request_common.h"

using namespace OHOS;
using namespace OHOS::Request;
using namespace OHOS::AniUtil;

template<>
bool UnionAccessor::TryConvertArray<ani_ref>(std::vector<ani_ref> &value)
{
    ani_double length;
    if (ANI_OK != env_->Object_GetPropertyByName_Double(obj_, "length", &length)) {
        return false;
    }

    for (int i = 0; i < int(length); i++) {
        ani_ref ref;
        if (ANI_OK != env_->Object_CallMethodByName_Ref(obj_, "$_get", "i:Y", &ref, (ani_int)i)) {
            return false;
        }
        value.push_back(ref);
    }
    return true;
}

static void ThrowBusinessError(ani_env *env, int errCode, std::string&& errMsg)
{
    REQUEST_HILOGI("into ThrowBusinessError.");
    static const char *errorClsName = "@ohos.base.BusinessError";
    ani_class cls {};
    if (env->FindClass(errorClsName, &cls) != ANI_OK) {
        REQUEST_HILOGE("find class BusinessError %{public}s failed", errorClsName);
        return;
    }
    ani_method ctor;
    if (env->Class_FindMethod(cls, "<ctor>", ":", &ctor) != ANI_OK) {
        REQUEST_HILOGE("find method BusinessError.constructor failed");
        return;
    }
    ani_object errorObject;
    if (env->Object_New(cls, ctor, &errorObject) != ANI_OK) {
        REQUEST_HILOGE("create BusinessError object failed");
        return;
    }
    ani_double aniErrCode = static_cast<ani_double>(errCode);
    ani_string errMsgStr;
    if (env->String_NewUTF8(errMsg.c_str(), errMsg.size(), &errMsgStr) != ANI_OK) {
        REQUEST_HILOGE("convert errMsg to ani_string failed");
        return;
    }
    if (env->Object_SetFieldByName_Double(errorObject, "code", aniErrCode) != ANI_OK) {
        REQUEST_HILOGE("set error code failed");
        return;
    }
    if (env->Object_SetPropertyByName_Ref(errorObject, "message", errMsgStr) != ANI_OK) {
        REQUEST_HILOGE("set error message failed");
        return;
    }
    env->ThrowError(static_cast<ani_error>(errorObject));
    return;
}

static ExceptionError InitConfig(ani_env *env, ani_object object, Config &config)
{
    std::shared_ptr<OHOS::AbilityRuntime::Context> context = nullptr;
    ExceptionError error = { .code = E_OK };
    context = JsInitialize::GetContext(env, object);
    if (context == nullptr) {
        REQUEST_HILOGE("context == null");
        error.code = E_PARAMETER_CHECK;
        error.errInfo = "Parameter verification failed, Get context fail";
        return error;
    }
    auto applicationInfo = context->GetApplicationInfo();
    if (applicationInfo == nullptr) {
        REQUEST_HILOGE("ApplicationInfo == null");
        error.code = E_OTHER;
        error.errInfo = "ApplicationInfo is null";
        return error;
    }
    config.bundleType = static_cast<u_int32_t>(applicationInfo->bundleType);
    config.bundleName = context->GetBundleName();
    config.version = Version::API10;
    bool ret = JsInitialize::CheckFilePath(context, config, error);
    if (!ret) {
        REQUEST_HILOGE("error info is: %{public}s", error.errInfo.c_str());
    }
    return error;
}

static bool IsArray(ani_env *env, ani_object aniData)
{
    ani_double length;
    if (ANI_OK != env->Object_GetPropertyByName_Double(aniData, "length", &length)) {
        return false;
    }
    return true;
}

ani_boolean OHOS::AniUtil::IsInstanceOf(ani_env *env, const std::string &cls_name, ani_object obj)
{
    ani_class cls;
    if (ANI_OK != env->FindClass(cls_name.c_str(), &cls)) {
        return ANI_FALSE;
    }

    ani_boolean ret;
    env->Object_InstanceOf(obj, cls, &ret);
    return ret;
}

static bool GetDownloadData(ani_env *env, Config &aniConfig, ani_object aniData)
{
    UnionAccessor unionAccessor(env, aniData);
    if (unionAccessor.IsInstanceOf("std.core.String")) {
        aniConfig.data = AniStringUtils::ToStd(env, static_cast<ani_string>(aniData));
    }
    return true;
}

static bool ProcessDatas(ani_env *env, Config &aniConfig, ani_object aniData)
{
    UnionAccessor unionAccessor(env, aniData);
    if (aniConfig.action == Action::DOWNLOAD) {
        return GetDownloadData(env, aniConfig, aniData);
    }
    if (aniConfig.action != Action::UPLOAD) {
        return false;
    }

    std::vector<ani_ref> arrayDoubleValues = {};
    if (!unionAccessor.TryConvertArray<ani_ref>(arrayDoubleValues) || arrayDoubleValues.empty()) {
        return false;
    }

    for (uint16_t i = 0; i < arrayDoubleValues.size(); i++) {
        ani_object data = static_cast<ani_object>(arrayDoubleValues[i]);
        ani_ref nameRef;
        if (ANI_OK != env->Object_GetPropertyByName_Ref(data, "name", &nameRef)) {
            REQUEST_HILOGE("Object_GetFieldByName_Ref name from data Faild");
            return false;
        }
        auto name = AniStringUtils::ToStd(env, static_cast<ani_string>(nameRef));

        ani_ref valueRef;
        if (ANI_OK != env->Object_GetPropertyByName_Ref(data, "value", &valueRef)) {
            REQUEST_HILOGE("Object_GetFieldByName_Ref value from data Faild");
            return false;
        }
        if (IsInstanceOf(env, "std.core.String", static_cast<ani_object>(valueRef))) {
            FormItem form;
            form.name = name;
            form.value = AniStringUtils::ToStd(env, static_cast<ani_string>(valueRef));
            aniConfig.forms.push_back(form);
            continue;
        }
        if (IsInstanceOf(env, "@ohos.request.request.agent.FileSpec", static_cast<ani_object>(valueRef))) {
            FileSpec file;
            if (!JsInitialize::Convert2FileSpec(env, static_cast<ani_object>(valueRef), name, file)) {
                REQUEST_HILOGE("Convert2FileSpec failed");
                return false;
            }
            aniConfig.files.push_back(file);
            continue;
        }
        if (!IsArray(env, static_cast<ani_object>(valueRef))) {
            return false;
        }
        if (!JsInitialize::Convert2FileSpecs(env, static_cast<ani_object>(valueRef), name, aniConfig.files)) {
            return false;
        }
    }
    return true;
}

static bool SetConfigInfo(ani_env *env, Config &aniConfig, ani_object config)
{
    ani_ref url;
    if (ANI_OK != env->Object_GetPropertyByName_Ref(config, "url", &url)) {
        REQUEST_HILOGI("Failed to get property named type");
        return false;
    }
    auto urlStr = AniStringUtils::ToStd(env, static_cast<ani_string>(url));
    REQUEST_HILOGI("urlStr: %{public}s", urlStr.c_str());
    aniConfig.url = urlStr;
    ani_ref aniAction;
    if (ANI_OK != env->Object_GetPropertyByName_Ref(config, "action", &aniAction)) {
        REQUEST_HILOGI("Failed to get property named type");
        return false;
    }
    EnumAccessor actionAccessor(env, static_cast<ani_enum_item>(aniAction));
    expected<Action, ani_status> actionExpected = actionAccessor.To<Action>();
    if (!actionExpected) {
        return false;
    }
    Action action = actionExpected.value();
    aniConfig.action = action;
    REQUEST_HILOGI("vibrateInfo.type: %{public}d", action);

    aniConfig.overwrite = true;
    
    ani_ref aniMethod;
    if (env->Object_GetPropertyByName_Ref(config, "method", &aniMethod) == ANI_OK && aniMethod != nullptr) {
        auto method = AniStringUtils::ToStd(env, static_cast<ani_string>(aniMethod));
        aniConfig.method = method;
    }

    ani_ref aniSaveas;
    if (env->Object_GetPropertyByName_Ref(config, "saveas", &aniSaveas) == ANI_OK && aniSaveas != nullptr) {
        auto saveas = AniStringUtils::ToStd(env, static_cast<ani_string>(aniSaveas));
        aniConfig.saveas = saveas;
    }

    ani_ref aniData;
    if (env->Object_GetPropertyByName_Ref(config, "data", &aniData) == ANI_OK && aniData != nullptr) {
        bool ret = ProcessDatas(env, aniConfig, static_cast<ani_object>(aniData));
        if (!ret) {
            REQUEST_HILOGE("ProcessDatas data error.");
            return ret;
        }
    }
    return true;
}

static ani_object Create([[maybe_unused]] ani_env *env, ani_object object, ani_object config)
{
    REQUEST_HILOGI("Create Start");
    ani_object nullobj{};
    if (object == nullptr) {
        REQUEST_HILOGE("context == null");
        return nullobj;
    }
    if (config == nullptr) {
        REQUEST_HILOGE("config == null");
        return nullobj;
    }

    Config aniConfig{};
    aniConfig.saveas = "default.txt";
    if (!SetConfigInfo(env, aniConfig, config)) {
        REQUEST_HILOGE("Failed to SetConfigInfo.");
        return nullobj;
    }

    ExceptionError err = InitConfig(env, object, aniConfig);
    if (err.code != E_OK) {
        REQUEST_HILOGE("err.code : %{public}d, err.errInfo :  %{public}s", err.code, err.errInfo.c_str());
        ThrowBusinessError(env, err.code, std::move(err.errInfo));
        return nullobj;
    }

    AniTask *task = AniTask::Create(env, aniConfig);
    if (task == nullptr) {
        REQUEST_HILOGE("AniTask::Create task == nullptr!");
        return nullobj;
    }

    auto taskImpl = AniObjectUtils::Create(env, "@ohos.request.request", "agent", "TaskImpl");

    NativePtrWrapper wrapper(env, taskImpl);
    wrapper.Wrap<AniTask>(task);
    return taskImpl;
}

static void StartSync([[maybe_unused]] ani_env *env, ani_object object)
{
    REQUEST_HILOGI("Enter Start");
    if (env == nullptr) {
        return;
    }
    NativePtrWrapper wrapper(env, object);
    auto task = wrapper.Unwrap<AniTask>();
    if (task == nullptr) {
        REQUEST_HILOGE("task is nullptr");
        return;
    }
    task->Start(env);
}

static void OnSync([[maybe_unused]] ani_env *env, [[maybe_unused]] ani_object object,
    ani_string response, ani_object callback)
{
    REQUEST_HILOGI("Enter On");

    ani_ref callbackRef = nullptr;
    env->GlobalReference_Create(reinterpret_cast<ani_ref>(callback), &callbackRef);
    auto responseEvent = AniStringUtils::ToStd(env, static_cast<ani_string>(response));
    NativePtrWrapper wrapper(env, object);
    auto task = wrapper.Unwrap<AniTask>();
    if (task == nullptr) {
        REQUEST_HILOGE("task is nullptr");
        return;
    }
    task->On(env, responseEvent, callbackRef);
}

ANI_EXPORT ani_status ANI_Constructor(ani_vm *vm, uint32_t *result)
{
    REQUEST_HILOGI("Enter ANI_Constructor Start");
    ani_env *env;
    if (ANI_OK != vm->GetEnv(ANI_VERSION_1, &env)) {
        REQUEST_HILOGI("Unsupported ANI_VERSION_1");
        return ANI_ERROR;
    }

    static const char *agentNamespaceName = "@ohos.request.request.agent";
    ani_namespace agent;
    if (ANI_OK != env->FindNamespace(agentNamespaceName, &agent)) {
        REQUEST_HILOGI("Not found '%{public}s'", agentNamespaceName);
        return ANI_ERROR;
    }
    std::array nsMethods = {
        ani_native_function {"createSync", nullptr, reinterpret_cast<void *>(Create)},
    };

    if (ANI_OK != env->Namespace_BindNativeFunctions(agent, nsMethods.data(), nsMethods.size())) {
        REQUEST_HILOGI("Cannot bind native methods to '%{public}s'", namespaceName);
        return ANI_ERROR;
    };

    static const char *requestclsName = "@ohos.request.request.agent.TaskImpl";
    ani_class requestClass;
    if (ANI_OK != env->FindClass(requestclsName, &requestClass)) {
        REQUEST_HILOGI("Not found class %{public}s", requestclsName);
        return ANI_NOT_FOUND;
    }

    std::array methods = {
        ani_native_function {"startSync", nullptr, reinterpret_cast<void *>(StartSync)},
        ani_native_function {"onSync", nullptr, reinterpret_cast<void *>(OnSync)},
    };

    if (ANI_OK != env->Class_BindNativeMethods(requestClass, methods.data(), methods.size())) {
        REQUEST_HILOGI("Cannot bind native methods to %{public}s", requestclsName);
        return ANI_ERROR;
    }

    auto cleanerCls = TypeFinder(env).FindClass("ohos.request.request.agent.Cleaner");
    NativePtrCleaner(env).Bind(cleanerCls.value());

    *result = ANI_VERSION_1;
    return ANI_OK;
}