/*
 * Copyright 2025 the original author or authors.
 *
 * 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
 *
 *      https://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.alibaba.cloud.ai.studio.admin.builder.controller;

import com.alibaba.cloud.ai.studio.runtime.exception.BizException;
import com.alibaba.cloud.ai.studio.runtime.enums.AppComponentStatusEnum;
import com.alibaba.cloud.ai.studio.runtime.enums.AppStatus;
import com.alibaba.cloud.ai.studio.runtime.enums.AppType;
import com.alibaba.cloud.ai.studio.runtime.enums.ErrorCode;
import com.alibaba.cloud.ai.studio.runtime.domain.PagingList;
import com.alibaba.cloud.ai.studio.runtime.domain.RequestContext;
import com.alibaba.cloud.ai.studio.runtime.domain.Result;
import com.alibaba.cloud.ai.studio.runtime.domain.app.Application;
import com.alibaba.cloud.ai.studio.runtime.domain.app.ApplicationVersion;
import com.alibaba.cloud.ai.studio.runtime.domain.component.AppComponent;
import com.alibaba.cloud.ai.studio.runtime.domain.component.AppComponentConfig;
import com.alibaba.cloud.ai.studio.runtime.domain.component.AppComponentQuery;
import com.alibaba.cloud.ai.studio.runtime.domain.refer.Refer;
import com.alibaba.cloud.ai.studio.runtime.domain.workflow.Node;
import com.alibaba.cloud.ai.studio.runtime.domain.workflow.NodeTypeEnum;
import com.alibaba.cloud.ai.studio.runtime.utils.JsonUtils;
import com.alibaba.cloud.ai.studio.core.base.service.AppComponentService;
import com.alibaba.cloud.ai.studio.core.base.service.AppService;
import com.alibaba.cloud.ai.studio.core.base.service.ReferService;
import com.alibaba.cloud.ai.studio.core.context.RequestContextHolder;
import com.alibaba.cloud.ai.studio.core.base.manager.AppComponentManager;
import com.alibaba.cloud.ai.studio.core.workflow.WorkflowConfig;
import com.alibaba.cloud.ai.studio.core.workflow.processor.impl.EndExecuteProcessor;
import com.alibaba.cloud.ai.studio.admin.builder.annotation.ApiModelAttribute;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;

import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Controller for managing application components in the SAA Studio platform. This
 * controller provides endpoints for: 1. CRUD operations on application components 2.
 * Component configuration management 3. Component reference relationship queries 4.
 * Component schema queries 5. Publishable application queries
 * <p>
 * The component system allows applications to be published as reusable components that
 * can be referenced by other applications, enabling modular application development.
 *
 * @author guning.lt
 * @since 1.0.0.3
 */
@RestController
@Tag(name = "app_component")
@RequestMapping("/console/v1/component-servers")
public class AppComponentController {

	private final AppComponentService appComponentService;

	private final AppService appService;

	private final AppComponentManager appComponentManager;

	private final ReferService referService;

	public AppComponentController(AppComponentService appComponentService, AppService appService,
			AppComponentManager appComponentManager, ReferService referService) {
		this.appComponentService = appComponentService;
		this.appService = appService;
		this.appComponentManager = appComponentManager;
		this.referService = referService;
	}

	/**
	 * Retrieves a paginated list of application components based on query parameters.
	 * @param request Query parameters for filtering components including: - type:
	 * Component type - name: Component name - appId: Associated application ID - status:
	 * Component status - pageSize: Number of items per page - pageNum: Page number
	 * @return Result containing a PagingList of AppComponent objects
	 */
	@GetMapping()
	public Result<PagingList<AppComponent>> getAppComponentPageList(@ApiModelAttribute AppComponentQuery request) {

		RequestContext context = RequestContextHolder.getRequestContext();
		try {
			PagingList<AppComponent> appComponentList = appComponentService.getAppComponentList(request);
			return Result.success(context.getRequestId(), appComponentList);
		}
		catch (Exception e) {
			throw new BizException(ErrorCode.APP_COMPONENT_LIST_ERROR.toError());
		}
	}

	/**
	 * Retrieves a paginated list of applications that can be published as components.
	 * This endpoint filters out applications that are already published as components.
	 * @param request Query parameters including: - type: Application type - appName:
	 * Application name
	 * @return Result containing a PagingList of Application objects that can be published
	 * as components
	 */
	@GetMapping("/app-publishable")
	public Result<PagingList<Application>> getAppPublishablePageList(@ApiModelAttribute AppComponentQuery request) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (Objects.isNull(request.getType())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("type"));
		}

		// component published
		List<AppComponent> appComponentList = appComponentService.getAppComponentListAll(request.getType(),
				AppComponentStatusEnum.Published.getCode());
		List<String> codes = new ArrayList<>();
		if (appComponentList != null && !appComponentList.isEmpty()) {
			codes = appComponentList.stream().map(AppComponent::getAppId).collect(Collectors.toList());
		}
		// application can released to component
		List<Long> ids = appService.getApplicationPublished(request.getType(), request.getAppName(), codes);

		PagingList<Application> componentList = appService.getApplicationPublishedAndNotComponentList(request, codes,
				ids);
		return Result.success(context.getRequestId(), componentList);
	}

	/**
	 * Publishes a new application component. Creates a new component from an existing
	 * application with specified configuration.
	 * @param request Component details including: - type: Component type - name:
	 * Component name - config: Component configuration - appId: Source application ID -
	 * description: Component description
	 * @return Result containing the unique code of the published component
	 */
	@PostMapping()
	public Result<String> publishComponent(@RequestBody AppComponentQuery request) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (Objects.isNull(request.getType())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("type"));
		}
		if (StringUtils.isBlank(request.getName())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("name"));
		}
		if (StringUtils.isBlank(request.getConfig())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("config"));
		}
		if (StringUtils.isBlank(request.getAppId())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("appId"));
		}
		if (StringUtils.isBlank(request.getDescription())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("description"));
		}

		// create component
		AppComponent component = initComponent(request);
		Result<String> appComponent = appComponentService.createAppComponent(component);
		if (appComponent.isSuccess()) {
			return Result.success(context.getRequestId(), appComponent.getData());
		}
		else {
			throw new BizException(ErrorCode.APP_COMPONENT_PUBLISH_ERROR.toError());
		}
	}

	/**
	 * Updates an existing application component. Modifies the configuration and details
	 * of a published component.
	 * @param code Unique code of the component to update
	 * @param request Updated component details including: - type: Component type - name:
	 * Component name - config: Updated configuration - appId: Associated application ID -
	 * description: Updated description
	 * @return Result indicating successful update
	 */
	@PutMapping("/{code}")
	public Result<String> updateComponent(@PathVariable("code") String code, @RequestBody AppComponentQuery request) {
		RequestContext context = RequestContextHolder.getRequestContext();
		request.setCode(code);
		if (Objects.isNull(request.getType())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("type"));
		}
		if (StringUtils.isBlank(request.getName())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("name"));
		}
		if (StringUtils.isBlank(request.getConfig())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("config"));
		}
		if (StringUtils.isBlank(request.getAppId())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("appId"));
		}
		if (StringUtils.isBlank(request.getDescription())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("description"));
		}
		if (StringUtils.isBlank(request.getCode())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("code"));
		}

		// update component config
		AppComponent component = initComponent(request);
		Result<Integer> integerResult = appComponentService.updateAppComponent(component);
		if (integerResult.isSuccess()) {
			return Result.success(context.getRequestId(), "update success");
		}
		else {
			throw new BizException(ErrorCode.APP_COMPONENT_UPDATE_ERROR.toError());
		}

	}

	/**
	 * Deletes an application component. Removes a component from the system by its unique
	 * code.
	 * @param code Unique code of the component to delete
	 * @return Result containing boolean indicating successful deletion
	 */
	@DeleteMapping("/{code}")
	public Result<Boolean> deleteComponent(@PathVariable("code") String code) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(code)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("code"));
		}
		Result<Void> voidResult = appComponentService.deleteAppComponent(code);
		if (voidResult.isSuccess()) {
			return Result.success(context.getRequestId(), true);
		}
		else {
			throw new BizException(ErrorCode.APP_COMPONENT_DELETE_ERROR.toError());
		}
	}

	/**
	 * Retrieves detailed information about a component by its unique code. Includes
	 * merged configuration from both component and source application.
	 * @param code Unique code of the component
	 * @return Result containing detailed AppComponent information with merged
	 * configuration
	 */
	@GetMapping("/{code}/detail-by-code")
	public Result<AppComponent> detailByCode(@PathVariable("code") String code) {

		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(code)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("code"));
		}
		try {
			AppComponent appComponentByCode = appComponentService.getAppComponentByCode(code,
					AppComponentStatusEnum.Published.getCode());
			if (appComponentByCode == null) {
				return Result.success(context.getRequestId(), null);
			}
			// get component inputScheme
			AppComponentConfig appComponentConfig = appComponentManager.getAppComponentInputConfig(appComponentByCode);
			String appId = appComponentByCode.getAppId();
			Application application = appService.getApp(appId);
			if (application == null) {
				return Result.error(context.getRequestId(), ErrorCode.APP_NOT_FOUND);
			}
			// get application inputScheme
			AppComponentConfig applicationInputConfig = appComponentManager.getApplicationInputConfig(application,
					false);
			// merge component config
			applicationInputConfig = appComponentManager.mergeConfig(applicationInputConfig, appComponentConfig);
			appComponentByCode.setConfig(JsonUtils.toJson(applicationInputConfig));
			return Result.success(context.getRequestId(), appComponentByCode);
		}
		catch (Exception e) {
			throw new BizException(ErrorCode.APP_COMPONENT_DETAIL_ERROR.toError());
		}

	}

	/**
	 * Retrieves detailed information about a component by its application ID. Includes
	 * merged configuration from both component and source application.
	 * @param appId Application ID associated with the component
	 * @return Result containing detailed AppComponent information with merged
	 * configuration
	 */
	@GetMapping("/{appId}/detail-by-appid")
	public Result<AppComponent> detailByAppId(@PathVariable("appId") String appId) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(appId)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("appId"));
		}

		try {
			AppComponent appComponentByAppId = appComponentService.getAppComponentByAppId(appId,
					AppComponentStatusEnum.Published.getCode());
			if (appComponentByAppId == null) {
				return Result.success(context.getRequestId(), null);
			}
			// get component inputScheme
			AppComponentConfig appComponentConfig = appComponentManager.getAppComponentInputConfig(appComponentByAppId);

			Application application = appService.getApp(appId);
			if (application == null) {
				throw new BizException(ErrorCode.APP_NOT_FOUND.toError());
			}
			// get application inputScheme
			AppComponentConfig applicationInputConfig = appComponentManager.getApplicationInputConfig(application,
					false);
			// merge component config
			applicationInputConfig = appComponentManager.mergeConfig(applicationInputConfig, appComponentConfig);
			appComponentByAppId.setConfig(JsonUtils.toJson(applicationInputConfig));
			return Result.success(appComponentByAppId);

		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_DETAIL_ERROR.toError());
		}

	}

	/**
	 * Queries components that reference a specific component.
	 * @param code Unique code of the component to find references for
	 * @return Result containing a list of components that reference the specified
	 * component
	 */
	@GetMapping("/{code}/query-refer")
	public Result<List<AppComponent>> queryReferInfo(@PathVariable("code") String code) {

		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(code)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("code"));
		}
		try {
			List<AppComponent> appComponentDTOList = new ArrayList<>();
			List<Refer> refers = referService.getReferListByReferCode(code);
			if (!CollectionUtils.isEmpty(refers)) {
				for (Refer refer : refers) {
					Application application = appService.getApp(refer.getMainCode());
					if (application == null || application.getStatus() == AppStatus.DELETED) {
						continue;
					}
					AppComponent appComponent = new AppComponent();
					appComponent.setAppId(application.getAppId());
					appComponent.setName(application.getName());
					AppType type = application.getType();
					appComponent.setType(type.getValue());
					appComponentDTOList.add(appComponent);
				}
			}
			return Result.success(context.getRequestId(), appComponentDTOList);
		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_REFER_ERROR.toError());
		}

	}

	/**
	 * Queries the configuration of a component by its application ID.
	 * @param appId Application ID associated with the component
	 * @return Result containing the component's configuration details
	 */
	@GetMapping("/{appId}/query-config")
	public Result<AppComponent> queryConfig(@PathVariable("appId") String appId) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(appId)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("appId"));
		}
		try {
			Application application = appService.getApp(appId);
			if (application == null) {
				throw new BizException(ErrorCode.APP_NOT_FOUND.toError());
			}
			AppComponentConfig applicationInputConfig = appComponentManager.getApplicationInputConfig(application,
					true);
			if (applicationInputConfig != null) {
				AppComponent appComponentDTO = new AppComponent();
				appComponentDTO.setConfig(JsonUtils.toJson(applicationInputConfig));
				appComponentDTO.setAppName(application.getName());
				appComponentDTO.setAppId(application.getAppId());
				return Result.success(context.getRequestId(), appComponentDTO);
			}
			else {
				throw new BizException(ErrorCode.APP_COMPONENT_QUERYCONFIG_ERROR.toError());
			}
		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_QUERYCONFIG_ERROR.toError());
		}

	}

	/**
	 * Retrieves a list of components by their unique codes.
	 * @param request Query containing a list of component codes to retrieve
	 * @return Result containing a list of matching AppComponent objects
	 */
	@PostMapping("/query-by-codes")
	public Result<List<AppComponent>> listByCodes(@RequestBody AppComponentQuery request) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (CollectionUtils.isEmpty(request.getCodes())) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("codes"));
		}
		try {
			List<AppComponent> appComponentListByCodes = appComponentService
				.getAppComponentListByCodes(request.getCodes());
			return Result.success(context.getRequestId(), appComponentListByCodes);

		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_QUERYCONFIG_ERROR.toError());
		}
	}

	/**
	 * Retrieves the schema of a component by its unique code. The schema includes
	 * input/output parameters and output type information.
	 * @param code Unique code of the component
	 * @return Result containing a map of schema information including: - input: Input
	 * parameter definitions - output: Output parameter definitions - output_type: Type of
	 * output (text/json)
	 */
	@GetMapping("/{code}/query-schema")
	public Result<Map<String, Object>> getComponentSchema(@PathVariable("code") String code) {
		RequestContext context = RequestContextHolder.getRequestContext();
		if (StringUtils.isBlank(code)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("code"));
		}
		try {
			Map<String, Object> objectObjectMap = getSchemas(code);
			return Result.success(context.getRequestId(), objectObjectMap);
		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_QUERYCONFIG_ERROR.toError());
		}

	}

	/**
	 * Retrieves schemas for multiple components by their unique codes.
	 * @param request Query containing a list of component codes
	 * @return Result containing a map of component codes to their respective schemas
	 */
	@PostMapping("/schema-by-codes")
	public Result<Map<String, Object>> getComponentSchemaByCodes(@RequestBody AppComponentQuery request) {
		List<String> codes = request.getCodes();
		RequestContext context = RequestContextHolder.getRequestContext();
		if (CollectionUtils.isEmpty(codes)) {
			throw new BizException(ErrorCode.MISSING_PARAMS.toError("codes"));
		}
		try {
			HashMap<String, Object> schemas = new HashMap<>();
			for (String code : codes) {
				Map<String, Object> objectObjectMap = getSchemas(code);
				schemas.put(code, objectObjectMap);
			}
			return Result.success(context.getRequestId(), schemas);
		}
		catch (Exception exception) {
			throw new BizException(ErrorCode.APP_COMPONENT_QUERYCONFIG_ERROR.toError());
		}
	}

	private AppComponent initComponent(AppComponentQuery component) {
		AppComponent appComponent = new AppComponent();
		if (component.getAppName() != null) {
			appComponent.setAppName(component.getAppName());
		}
		if (component.getAppId() != null) {
			appComponent.setAppId(component.getAppId());
		}
		if (component.getCode() != null) {
			appComponent.setCode(component.getCode());
		}
		if (component.getName() != null) {
			appComponent.setName(component.getName());
		}
		if (component.getType() != null) {
			appComponent.setType(component.getType());
		}
		if (component.getDescription() != null) {
			appComponent.setDescription(component.getDescription());
		}
		if (component.getConfig() != null) {
			appComponent.setConfig(component.getConfig());
		}
		appComponent.setStatus(AppComponentStatusEnum.Published.getCode());
		return appComponent;
	}

	private Map<String, Object> getSchemas(String code) {
		AppComponent appComponentByCode = appComponentService.getAppComponentByCode(code,
				AppComponentStatusEnum.Published.getCode());
		Map<String, List<AppComponentConfig.Params>> stringListMap = appComponentManager
			.fetchInputAndOutputParams(appComponentByCode);
		List<AppComponentConfig.Params> input = stringListMap.get("input");
		for (AppComponentConfig.Params appComponentParam : input) {
			appComponentParam.setField(appComponentParam.getAlias());
		}
		String json = JsonUtils.toJson(stringListMap);
		Map<String, Object> objectObjectMap = JsonUtils.fromJsonToMap(json);
		objectObjectMap.put("output_type", "text");
		if (appComponentByCode.getType().equals(AppType.WORKFLOW.getValue())) {
			ApplicationVersion appVersion = appService.getAppVersion(appComponentByCode.getAppId(), "lastPublished");
			if (appVersion == null) {
				throw new BizException(ErrorCode.APP_NOT_FOUND.toError());
			}
			WorkflowConfig appOrchestraConfig = JsonUtils.fromJson(appVersion.getConfig(), WorkflowConfig.class);
			List<Node> nodes = appOrchestraConfig.getNodes()
				.stream()
				.filter(nodeTmp -> nodeTmp.getType().equals(NodeTypeEnum.END.getCode()))
				.toList();
			Node endNode = nodes.get(0);
			EndExecuteProcessor.NodeParam endNodeParam = JsonUtils.fromMap(endNode.getConfig().getNodeParam(),
					EndExecuteProcessor.NodeParam.class);

			if ("json".equals(endNodeParam.getOutputType())) {
				objectObjectMap.put("output_type", "json");
			}
		}
		return objectObjectMap;
	}

}
