/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the "Elastic License
 * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
 * Public License v 1"; you may not use this file except in compliance with, at
 * your election, the "Elastic License 2.0", the "GNU Affero General Public
 * License v3.0 only", or the "Server Side Public License, v 1".
 */

package org.elasticsearch.rest.action.cat;

import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.cluster.metadata.ComponentTemplate;
import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.cluster.project.ProjectIdResolver;
import org.elasticsearch.common.Table;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.Scope;
import org.elasticsearch.rest.ServerlessScope;
import org.elasticsearch.rest.action.RestResponseListener;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout;

/**
 * cat API class for handling get componentTemplate.
 */
@ServerlessScope(Scope.PUBLIC)
public class RestCatComponentTemplateAction extends AbstractCatAction {

    private final ProjectIdResolver projectIdResolver;

    public RestCatComponentTemplateAction(ProjectIdResolver projectIdResolver) {
        this.projectIdResolver = projectIdResolver;
    }

    @Override
    public String getName() {
        return "cat_component_template_action";
    }

    @Override
    public List<Route> routes() {
        return List.of(new Route(GET, "/_cat/component_templates"), new Route(GET, "/_cat/component_templates/{name}"));
    }

    @Override
    protected void documentation(StringBuilder sb) {
        sb.append("/_cat/component_templates\n");
    }

    @Override
    protected Table getTableWithHeader(RestRequest request) {
        Table table = new Table();
        table.startHeaders();
        table.addCell("name", "alias:n;desc:component template name");
        table.addCell("version", "alias:v;desc:version");
        table.addCell("alias_count", "alias:a;desc:alias count");
        table.addCell("mapping_count", "alias:m;desc:mapping count");
        table.addCell("settings_count", "alias:s;desc:settings count");
        table.addCell("metadata_count", "alias:me;desc:metadata count");
        table.addCell("included_in", "alias:i;desc:included in");
        table.endHeaders();
        return table;
    }

    @Override
    protected BaseRestHandler.RestChannelConsumer doCatRequest(RestRequest request, NodeClient client) {
        final String matchPattern = request.hasParam("name") ? request.param("name") : null;
        final ClusterStateRequest clusterStateRequest = new ClusterStateRequest(getMasterNodeTimeout(request));
        clusterStateRequest.clear().metadata(true);
        clusterStateRequest.local(request.paramAsBoolean("local", clusterStateRequest.local()));
        return channel -> client.admin().cluster().state(clusterStateRequest, new RestResponseListener<>(channel) {
            @Override
            public RestResponse buildResponse(ClusterStateResponse clusterStateResponse) throws Exception {
                return RestTable.buildResponse(buildTable(request, clusterStateResponse, matchPattern), channel);
            }
        });
    }

    public Table buildTable(RestRequest request, ClusterStateResponse clusterStateResponse, String patternString) throws Exception {
        Table table = getTableWithHeader(request);
        final Metadata metadata = clusterStateResponse.getState().metadata();
        if (metadata.projects().size() > 1) {
            throw new IllegalStateException("returned cluster state has multiple projects");
        }
        final ProjectMetadata project = metadata.getProject(projectIdResolver.getProjectId());
        Map<String, Set<String>> reverseIndexOnComposedOfToIndexName = buildReverseIndexOnComposedOfToIndexName(project);

        for (Map.Entry<String, ComponentTemplate> entry : project.componentTemplates().entrySet()) {
            String name = entry.getKey();
            ComponentTemplate componentTemplate = entry.getValue();
            if (patternString == null || Regex.simpleMatch(patternString, name)) {
                table.startRow();
                table.addCell(name);
                table.addCell(componentTemplate.version());
                Template template = componentTemplate.template();
                table.addCell(template == null || template.aliases() == null ? 0 : template.aliases().size());
                table.addCell(countMappingInTemplate(template));
                table.addCell(template == null || template.settings() == null ? 0 : template.settings().keySet().size());
                table.addCell(componentTemplate.metadata() == null ? 0 : componentTemplate.metadata().size());
                table.addCell(reverseIndexOnComposedOfToIndexName.getOrDefault(name, new HashSet<>()));
                table.endRow();
            }
        }
        return table;
    }

    private static Map<String, Set<String>> buildReverseIndexOnComposedOfToIndexName(ProjectMetadata project) {
        Map<String, ComposableIndexTemplate> allTemplates = project.templatesV2();
        Map<String, Set<String>> reverseIndex = new HashMap<>();

        for (Map.Entry<String, ComposableIndexTemplate> templateEntry : allTemplates.entrySet()) {
            String templateName = templateEntry.getKey();
            ComposableIndexTemplate template = templateEntry.getValue();
            template.composedOf().forEach(composed_of -> reverseIndex.computeIfAbsent(composed_of, k -> new HashSet<>()).add(templateName));
        }
        return reverseIndex;
    }

    private static int countMappingInTemplate(Template template) throws Exception {
        if (template.mappings() == null) {
            return 0;
        }
        int count = 0;
        XContentType xContentType = XContentType.JSON;
        try (
            XContentParser parser = xContentType.xContent()
                .createParser(XContentParserConfiguration.EMPTY, template.mappings().uncompressed().array())
        ) {
            XContentParser.Token token = parser.nextToken();
            String currentFieldName = null;
            while (token != XContentParser.Token.END_OBJECT) {
                if (token == XContentParser.Token.FIELD_NAME) {
                    currentFieldName = parser.currentName();
                } else if (token == XContentParser.Token.START_OBJECT) {
                    if ("_doc".equals(currentFieldName)) {
                        List<Object> list = parser.mapOrdered().values().stream().toList();
                        for (Object mapping : list) {
                            count = count + countSubAttributes(mapping);
                        }
                    }
                } else {
                    parser.skipChildren();
                }
                token = parser.nextToken();
            }
        }
        return count;
    }

    private static int countSubAttributes(Object mapping) {
        int count = 0;
        if (mapping instanceof List) {
            for (Object subObject : (List) mapping) {
                count = count + countSubAttributes(subObject);
            }
        } else if (mapping instanceof Map) {
            for (Object entry : ((Map) mapping).entrySet()) {
                count = count + countSubAttributes(entry);
            }
        } else {
            count = count + 1;
        }
        return count;
    }
}
