#include "json_writer.h"

#include "config.h"
#include "exception.h"
#include "magic_strings.h"
#include "moderation_status.h"
#include "permissions_tree_helpers.h"
#include "user_info.h"

#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>

#include <yandex/maps/wiki/common/date_time.h>
#include <yandex/maps/wiki/common/json_helpers.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <list>
#include <map>
#include <optional>
#include <sstream>
#include <utility>

namespace maps::wiki::aclsrv {

namespace {

class AclCache
{
public:
    explicit AclCache(acl::ACLGateway& aclGw)
        : aclGw_(aclGw)
    {}

    std::optional<acl::Aoi> aoi(acl::ID aoiId)
    {
        if (!aoiId) {
            return std::nullopt;
        }
        if (!aoiMap_.contains(aoiId)) {
            aoiMap_.emplace(aoiId, aclGw_.aoi(aoiId));
        }
        return aoiMap_.at(aoiId);
    }

    const acl::Role& role(acl::ID roleId)
    {
        if (!rolesMap_) {
            rolesMap_ = RolesMap();
            for (auto&& role : aclGw_.roles()) {
                auto roleId = role.id();
                rolesMap_->emplace(roleId, std::move(role));
            }
        }
        return rolesMap_->at(roleId);
    }

private:
    using AoiMap = std::map<acl::ID, acl::Aoi>;
    using RolesMap = std::map<acl::ID, acl::Role>;

    acl::ACLGateway& aclGw_;
    AoiMap aoiMap_;
    std::optional<RolesMap> rolesMap_;
};

void writeGroupBase(json::ObjectBuilder json, const acl::Group& group)
{
    json[ID] = common::idToJson(group.id());
    json[NAME] = group.name();
}

void writeAoiBase(json::ObjectBuilder json, const acl::Aoi& obj)
{
    json[ID] = common::idToJson(obj.id());
    json[TITLE] = obj.name();
}

void writeRoleBase(json::ObjectBuilder json, const acl::Role& role)
{
    json[ID] = common::idToJson(role.id());
    json[NAME] = role.name();
    json[PUBLIC] = role.isPublic();
}

void writeSchedules(
    json::ArrayBuilder schedulesArray,
    const std::vector<acl::Schedule>& schedules)
{
    for (const auto& schedule : schedules) {
        schedulesArray << [&](json::ObjectBuilder scheduleBuilder) {
            scheduleBuilder[DATE_START] = schedule.startDate();
            if (schedule.endDate()) {
                scheduleBuilder[DATE_END] = *schedule.endDate();
            }
            if (schedule.startTime()) {
                scheduleBuilder[TIME_START] = *schedule.startTime();
            }
            if (schedule.endTime()) {
                scheduleBuilder[TIME_END] = *schedule.endTime();
            }
            if (schedule.weekdays()) {
                scheduleBuilder[WEEKDAYS] = *schedule.weekdays();
            }
            if (schedule.workRestDays()) {
                scheduleBuilder[SEQUENCE] << [&](json::ArrayBuilder workRestDaysArray) {
                    for (const auto& pair : *schedule.workRestDays()) {
                        workRestDaysArray << [&](json::ArrayBuilder daysPairBuilder) {
                            daysPairBuilder << pair.first;
                            daysPairBuilder << pair.second;
                        };
                    }
                };
            }
        };
    }
}

void writePolicy(
    json::ObjectBuilder policyBuilder,
    const std::optional<const acl::Role>& role,
    const std::optional<acl::Aoi>& aoi,
    const std::optional<acl::Group>& providedByGroup = std::nullopt,
    const acl::Schedules& schedules = {})
{
    if (role) {
        policyBuilder[ROLE] << [&](json::ObjectBuilder roleBuilder) {
            writeRoleBase(roleBuilder, *role);
        };
    }
    if (aoi) {
        policyBuilder[AOI] << [&](json::ObjectBuilder aoiBuilder) {
            writeAoiBase(aoiBuilder, *aoi);
        };
    }
    if (providedByGroup) {
        policyBuilder[GROUP] << [&](json::ObjectBuilder groupBuilder) {
            writeGroupBase(groupBuilder, *providedByGroup);
        };
    }
    if (!schedules.empty()) {
        policyBuilder[SCHEDULES] << [&](json::ArrayBuilder schedulesArray) {
            writeSchedules(schedulesArray, schedules);
        };
    }
}

template<typename Builder>
void writeUser(
    Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::User& user,
    IsYandexRequest isYandexRequest,
    OutputFlagsSet outputFlags,
    const std::string& modStatus)
{
    jsonBuilder << [&](json::ObjectBuilder userBuilder) {
        userBuilder[UID] = common::idToJson(user.uid());
        userBuilder[ID] = common::idToJson(user.id());
        userBuilder[DISPLAY_NAME] = user.displayName();

        UserInfo userInfo(user, modStatus);
        if (isYandexRequest == IsYandexRequest::Yes || !userInfo.isYandex()) {
            userBuilder[LOGIN] = user.login();
            userBuilder[CREATED_AT] = common::canonicalDateTimeString(
                user.created(),
                common::WithTimeZone::Yes);
            userBuilder[MODIFIED_AT] = common::canonicalDateTimeString(
                user.modified(),
                common::WithTimeZone::Yes);
            userBuilder[MODIFIED_BY] = common::idToJson(user.modifiedBy());
        }

        userBuilder[STATUS] = toString(user.status());
        if (user.status() == acl::User::Status::Deleted &&
            user.deleteReason())
        {
            userBuilder[DELETE_REASON] = toString(*user.deleteReason());
        }
        userBuilder[TRUST_LEVEL] = toString(user.trustLevel());

        if (outputFlags.isSet(OutputFlags::WithModerationStatus)) {
            userBuilder[MODERATION_STATUS] = modStatus;
        }

        if (outputFlags.isSet(OutputFlags::WithYandex)) {
            userBuilder[YANDEX_FLAG] = userInfo.isYandex();
        }

        if (outputFlags.isSet(OutputFlags::WithOutsourcer)) {
            userBuilder[OUTSOURCER_FLAG] = userInfo.isOutsourcer();
        }

        if (outputFlags.isSet(OutputFlags::WithPieceworker)) {
            userBuilder[PIECEWORKER_FLAG] = userInfo.isPieceworker();
        }

        if (outputFlags.isSet(OutputFlags::WithBanableInfo)) {
            userBuilder[BANABLE_FLAG] = userInfo.isBanable();
        }

        if (outputFlags.isSet(OutputFlags::WithBanInfo)
            && user.currentBan())
        {
            write(userBuilder[BAN_RECORD], *user.currentBan());
        }

        if (!outputFlags.isSet(OutputFlags::WithPolicies)) {
            return;
        }

        AclCache aclCache(aclGw);
        userBuilder[POLICIES] << [&](json::ArrayBuilder policiesArray) {
            const auto combinedScheduledObjects = combineScheduledObjects(
                aclGw.scheduledObjectsForAgent(user.id()));

            for (const auto& kvp : combinedScheduledObjects.policies) {
                const auto roleId = kvp.first.first;
                const auto aoiId = kvp.first.second;
                const auto& schedules = kvp.second;
                policiesArray << [&](json::ObjectBuilder policyBuilder) {
                    writePolicy(
                        policyBuilder,
                        aclCache.role(roleId),
                        aclCache.aoi(aoiId),
                        std::nullopt, /* providedByGroup */
                        schedules);
                };
            }
            for (const auto& kvp : combinedScheduledObjects.groups) {
                const auto group = aclGw.group(kvp.first); // TODO: aclGw.groups(...)
                const auto& schedules = kvp.second;
                const auto groupPolicies = group.policies();
                if (groupPolicies.empty()) {
                    policiesArray << [&](json::ObjectBuilder policyBuilder) {
                        policyBuilder[GROUP] << [&](json::ObjectBuilder groupBuilder) {
                            writeGroupBase(groupBuilder, group);
                        };
                        policyBuilder[SCHEDULES] << [&](json::ArrayBuilder schedulesArray) {
                            writeSchedules(schedulesArray, schedules);
                        };
                    };
                } else {
                    for (const auto& policy : groupPolicies) {
                        policiesArray << [&](json::ObjectBuilder policyBuilder) {
                            writePolicy(
                                policyBuilder,
                                policy.role(),
                                aclCache.aoi(policy.aoiId()),
                                group,
                                schedules);
                        };
                    }
                }
            }

            for (const auto& policy : user.policies()) {
                const auto policyKey = std::make_pair(policy.roleId(), policy.aoiId());
                if (combinedScheduledObjects.policies.count(policyKey)) {
                    continue;
                }
                policiesArray << [&](json::ObjectBuilder policyBuilder) {
                    writePolicy(
                        policyBuilder,
                        policy.role(),
                        aclCache.aoi(policy.aoiId()));
                };
            }
            for (const auto& group : user.groups()) {
                if (combinedScheduledObjects.groups.count(group.id())) {
                    continue;
                }
                const auto groupPolicies = group.policies();
                if (groupPolicies.empty()) {
                    policiesArray << [&](json::ObjectBuilder policyBuilder) {
                        policyBuilder[GROUP] << [&](json::ObjectBuilder groupBuilder) {
                            writeGroupBase(groupBuilder, group);
                        };
                    };
                } else {
                    for (const auto& policy : groupPolicies) {
                        policiesArray << [&](json::ObjectBuilder policyBuilder) {
                            writePolicy(
                                policyBuilder,
                                policy.role(),
                                aclCache.aoi(policy.aoiId()),
                                group);
                        };
                    }
                }
            }
        };
    };
}

template<typename Builder>
void writeBanRecord(
    Builder& jsonBuilder,
    const acl::BanRecord& banRecord)
{
    jsonBuilder << [&](json::ObjectBuilder banRecBuilder) {
        banRecBuilder[UID] = banRecord.uid();
        banRecBuilder[ACTION] = toString(banRecord.action());
        banRecBuilder[ID] = banRecord.id();
        banRecBuilder[CREATED_BY] = banRecord.createdBy();
        banRecBuilder[CREATED_AT] = common::canonicalDateTimeString(
            banRecord.created(),
            common::WithTimeZone::Yes);
        if (!banRecord.expires().empty()) {
            banRecBuilder[EXPIRES] = common::canonicalDateTimeString(
                banRecord.expires(),
                common::WithTimeZone::Yes);
        }
        if (!banRecord.reason().empty()) {
            banRecBuilder[REASON] = banRecord.reason();
        }
    };
}

void writeAllPermissionsTree(
    json::ObjectBuilder jsonBuilder,
    const acl::Permissions& allPermissions,
    const std::vector<acl::Permission>& roots,
    std::list<std::string>& curPathStack)
{
    for (const auto& root : roots) {
        jsonBuilder[outputPermissionId(root.name())] << [&](json::ObjectBuilder permBuilder) {
            permBuilder[LABEL] = cfg()->permissionLabel(root.name(), curPathStack);
            curPathStack.push_back(root.name());
            const auto& children = allPermissions.children(root);
            if (!children.empty()) {
                permBuilder[CHILDREN] << [&](json::ObjectBuilder childrenBuilder) {
                    writeAllPermissionsTree(
                        childrenBuilder,
                        allPermissions,
                        children,
                        curPathStack);
                };
            }
            curPathStack.pop_back();
        };
    }
}

void writePermissionsTree(
    json::ObjectBuilder jsonBuilder,
    const std::list<TreeNode>& level)
{
    for (const auto& n : level) {
        if (n.children.empty()) {
            jsonBuilder[outputPermissionId(n.name)] << true;
        } else {
            jsonBuilder[outputPermissionId(n.name)] << [&](json::ObjectBuilder permission) {
                writePermissionsTree(permission, n.children);
            };
        }
    }
}

template<typename Builder>
void writePermissions(
    Builder& jsonBuilder,
    const acl::Permissions& allPermissions,
    const std::vector<acl::Permission>& permissions)
{
    auto permissionsTreeRoots = buildPermissionsTree(allPermissions, permissions);
    jsonBuilder << [&](json::ObjectBuilder permBuilder) {
        permBuilder[PERMISSIONS] << [&](json::ObjectBuilder treeBuilder) {
            writePermissionsTree(treeBuilder, permissionsTreeRoots);
        };
    };
}

template<>
void writePermissions(
    json::ObjectBuilder& jsonBuilder,
    const acl::Permissions& allPermissions,
    const std::vector<acl::Permission>& permissions)
{
    auto permissionsTreeRoots = buildPermissionsTree(allPermissions, permissions);
    jsonBuilder[PERMISSIONS] << [&](json::ObjectBuilder treeBuilder) {
        writePermissionsTree(treeBuilder, permissionsTreeRoots);
    };
}

template<typename Builder>
void writeRole(
    Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::Permissions& allPermissions,
    const acl::Role& role)
{
    jsonBuilder << [&](json::ObjectBuilder roleBuilder) {
        writeRoleBase(roleBuilder, role);
        roleBuilder[DESCRIPTION] = role.description();
        writePermissions(roleBuilder, allPermissions, role.permissions());
        roleBuilder[CAN_ASSIGN] << [&](json::ObjectBuilder canAssignBuilder) {
            canAssignBuilder[ROLES] << [&](json::ArrayBuilder rolesBuilder) {
                const auto& canAssignRoles = role.canAssignRoles();
                for (const auto& canAssignRole : canAssignRoles) {
                    rolesBuilder << [&](json::ObjectBuilder canAssignRoleBuilder) {
                        writeRoleBase(canAssignRoleBuilder, canAssignRole);
                        canAssignRoleBuilder[DESCRIPTION] = canAssignRole.description();
                    };
                }
            };
            canAssignBuilder[GROUPS] << [&](json::ArrayBuilder groupsBuilder) {
                const auto& canAssignGroups = role.canAssignGroups();
                for (const auto& canAssignGroup : canAssignGroups) {
                    write(groupsBuilder, aclGw, canAssignGroup, OutputFlags::WithPolicies);
                }
            };
        };
    };
}

template<typename JsonClass>
void writeGroup(
    JsonClass& jsonClass,
    acl::ACLGateway& aclGw,
    const acl::Group& group,
    AclCache& aclCache,
    OutputFlagsSet outputFlags)
{
    jsonClass << [&](json::ObjectBuilder groupBuilder) {
        writeGroupBase(groupBuilder, group);
        groupBuilder[DESCRIPTION] = group.description();
        if (!outputFlags.isSet(OutputFlags::WithPolicies)) {
            return;
        }
        const auto& groupPolicies = group.policies();
        const auto& groupScheduledObjects = aclGw.scheduledObjectsForAgent(group.id());
        if (groupPolicies.empty() && groupScheduledObjects.policies.empty()) {
            return;
        }
        const auto combinedScheduledObjects = combineScheduledObjects(groupScheduledObjects);

        groupBuilder[POLICIES] << [&](json::ArrayBuilder policiesArray) {
            for (const auto& kvp : combinedScheduledObjects.policies) {
                const auto roleId = kvp.first.first;
                const auto aoiId = kvp.first.second;
                const auto& schedules = kvp.second;
                policiesArray << [&](json::ObjectBuilder policyBuilder) {
                    writePolicy(
                        policyBuilder,
                        aclCache.role(roleId),
                        aclCache.aoi(aoiId),
                        std::nullopt, /* providedByGroup */
                        schedules);
                };
            }
            for (const auto& policy : groupPolicies) {
                const auto policyKey = std::make_pair(policy.roleId(), policy.aoiId());
                if (combinedScheduledObjects.policies.count(policyKey)) {
                    continue;
                }
                policiesArray << [&](json::ObjectBuilder policyBuilder) {
                    writePolicy(
                        policyBuilder,
                        policy.role(),
                        aclCache.aoi(policy.aoiId()));
                };
            }
        };
    };
}

} // namespace

void write(
    json::ArrayBuilder arrayBuilder,
    acl::ACLGateway& aclGw,
    const acl::User& user,
    IsYandexRequest isYandexRequest,
    OutputFlagsSet outputFlags,
    const std::string& modStatus)
{
    writeUser(arrayBuilder, aclGw, user, isYandexRequest, outputFlags, modStatus);
}

void write(
    json::VariantBuilder variantBuilder,
    acl::ACLGateway& aclGw,
    const acl::User& user,
    IsYandexRequest isYandexRequest,
    OutputFlagsSet outputFlags,
    const std::string& modStatus)
{
    writeUser(variantBuilder, aclGw, user, isYandexRequest, outputFlags, modStatus);
}

void write(
    json::ArrayBuilder arrayBuilder,
    const acl::BanRecord& banRecord)
{
    writeBanRecord(arrayBuilder, banRecord);
}

void write(
    json::VariantBuilder variantBuilder,
    const acl::BanRecord& banRecord)
{
    writeBanRecord(variantBuilder, banRecord);
}

void write(
    json::Builder& jsonBuilder,
    const acl::Permissions& allPermissions)
{
    std::list<std::string> curPathStack;
    jsonBuilder << [&](json::ObjectBuilder treeBuilder) {
        writeAllPermissionsTree(
            treeBuilder,
            allPermissions,
            allPermissions.roots(),
            curPathStack);
    };
}

void write(
    json::Builder& jsonBuilder,
    const acl::Permissions& allPermissions,
    const std::vector<acl::Permission>& permissions)
{
    writePermissions(jsonBuilder, allPermissions, permissions);
}

void write(
    json::VariantBuilder variantBuilder,
    const acl::Permissions& allPermissions,
    const std::vector<acl::Permission>& permissions)
{
    writePermissions(variantBuilder, allPermissions, permissions);
}

void write(
    json::Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::Permissions& allPermissions,
    const acl::Role& role)
{
    writeRole(jsonBuilder, aclGw, allPermissions, role);
}

void write(
    json::ArrayBuilder arrayBuilder,
    acl::ACLGateway& aclGw,
    const acl::Permissions& allPermissions,
    const acl::Role& role)
{
    writeRole(arrayBuilder, aclGw, allPermissions, role);
}

void write(
    json::Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::Permissions& allPermissions,
    const acl::ResultBeforeAfter<acl::Role>& rolesResult)
{
    jsonBuilder << [&](json::ObjectBuilder objectBuilder) {
        objectBuilder[HAS_MORE] = rolesResult.hasMore;
        objectBuilder[ROLES] << [&](json::ArrayBuilder arrayBuilder) {
            for (const auto& role : rolesResult.objects) {
                writeRole(arrayBuilder, aclGw, allPermissions, role);
            }
        };
    };
}

void write(
    json::Builder& jsonBuilder,
    const std::vector<PolicyWithGroup>& policiesWithGroups,
    CompleteResult completeResult)
{
    jsonBuilder << [&](json::ObjectBuilder objectBuilder) {
        objectBuilder[COMPLETE] = (completeResult == CompleteResult::True);
        objectBuilder[POLICIES] << [&](json::ArrayBuilder policiesArray) {
            for (const auto& policyWithGroup : policiesWithGroups) {
                policiesArray << [&](json::ObjectBuilder policyBuilder) {
                    writePolicy(
                        policyBuilder,
                        policyWithGroup.role,
                        policyWithGroup.aoi,
                        policyWithGroup.group,
                        policyWithGroup.schedules);
                };
            }
        };
    };
}

void write(
    json::Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::Group& group,
    OutputFlagsSet outputFlags)
{
    AclCache aclCache(aclGw);
    writeGroup(jsonBuilder, aclGw, group, aclCache, outputFlags);
}

void write(
    json::ArrayBuilder jsonArray,
    acl::ACLGateway& aclGw,
    const acl::Group& group,
    OutputFlagsSet outputFlags)
{
    AclCache aclCache(aclGw);
    writeGroup(jsonArray, aclGw, group, aclCache, outputFlags);
}

void write(
    json::Builder& jsonBuilder,
    acl::ACLGateway& aclGw,
    const acl::ResultBeforeAfter<acl::Group>& groupsResult,
    OutputFlagsSet outputFlags)
{
    AclCache aclCache(aclGw);
    jsonBuilder << [&](json::ObjectBuilder objectBuilder) {
        objectBuilder[HAS_MORE] = groupsResult.hasMore;
        objectBuilder[GROUPS] << [&](json::ArrayBuilder groupsArray) {
            for (const auto& group : groupsResult.objects) {
                writeGroup(groupsArray, aclGw, group, aclCache, outputFlags);
            }
        };
    };
}

void write(
    json::ObjectBuilder jsonBuilder,
    const common::Pager& pager)
{
    jsonBuilder[PAGER] << [&](json::ObjectBuilder pagerBuilder) {
        pagerBuilder[TOTAL_COUNT] = pager.totalCount();
        pagerBuilder[PER_PAGE] = pager.perPage();
        pagerBuilder[PAGE] = pager.page();
    };
}

void write(
    json::ArrayBuilder arrayBuilder,
    const std::vector<acl::Role>& roles)
{
    for (const auto& role : roles) {
        arrayBuilder << [&](json::ObjectBuilder roleBuilder) {
            writeRoleBase(roleBuilder, role);
            roleBuilder[DESCRIPTION] = role.description();
        };
    }
}

void write(
    json::ArrayBuilder arrayBuilder,
    acl::ACLGateway& aclGw,
    const std::vector<acl::Group>& groups)
{
    AclCache aclCache(aclGw);
    for (const auto& group : groups) {
        writeGroup(arrayBuilder, aclGw, group, aclCache, OutputFlags::WithPolicies);
    }
}

} // namespace maps::wiki::aclsrv
