package eu.dnetlib.dsm;

import static eu.dnetlib.dsm.utils.DsmMappingUtils.asDbEntry;
import static eu.dnetlib.dsm.utils.DsmMappingUtils.copyNonNullProperties;
import static eu.dnetlib.dsm.utils.DsmMappingUtils.createId;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.validation.Valid;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import eu.dnetlib.data.openaire.dsm.model.Api;
import eu.dnetlib.data.openaire.dsm.model.Datasource;
import eu.dnetlib.data.openaire.dsm.model.Identity;
import eu.dnetlib.dsm.domain.AggregationHistoryResponse;
import eu.dnetlib.dsm.domain.AggregationInfo;
import eu.dnetlib.dsm.domain.ApiDetails;
import eu.dnetlib.dsm.domain.ApiDetailsResponse;
import eu.dnetlib.dsm.domain.Country;
import eu.dnetlib.dsm.domain.DatasourceDetailResponse;
import eu.dnetlib.dsm.domain.DatasourceDetails;
import eu.dnetlib.dsm.domain.DatasourceDetailsUpdate;
import eu.dnetlib.dsm.domain.DatasourceDetailsWithApis;
import eu.dnetlib.dsm.domain.DatasourceSnippetResponse;
import eu.dnetlib.dsm.domain.RegisteredDatasourceInfo;
import eu.dnetlib.dsm.domain.RequestFilter;
import eu.dnetlib.dsm.domain.RequestSort;
import eu.dnetlib.dsm.domain.RequestSortOrder;
import eu.dnetlib.dsm.domain.SimpleResponse;
import eu.dnetlib.dsm.utils.DsmMappingUtils;
import eu.dnetlib.dsm.utils.ResponseUtils;
import eu.dnetlib.dsm.utils.WfLoggerClient;
import eu.dnetlib.is.errors.DsmException;
import eu.dnetlib.is.errors.DsmForbiddenException;
import eu.dnetlib.is.errors.DsmNotFoundException;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController
@RequestMapping("/api/dsm/1.0")
@ConditionalOnProperty(value = "openaire.api.enable.dsm", havingValue = "true")
@Tag(name = "OpenAIRE DSM API", description = "the OpenAIRE Datasource Manager API")
public class DsmApiControllerV1 extends AbstractDsmController {

	private static final Log log = LogFactory.getLog(DsmApiControllerV1.class);

	@Autowired
	private WfLoggerClient wfLoggerClient;

	@Autowired
	private DsmService dsmService;

	@GetMapping("/ds/countries")
	public List<Country> listCountries() throws DsmException {
		return dsmService.listCountries();
	}

	@PostMapping("/ds/searchdetails/{page}/{size}")
	public DatasourceDetailResponse searchDsDetails(
		@RequestParam final RequestSort requestSortBy,
		@RequestParam final RequestSortOrder order,
		@RequestBody final RequestFilter requestFilter,
		@PathVariable final int page,
		@PathVariable final int size) throws DsmException {
		final StopWatch stop = StopWatch.createStarted();

		final DatasourceDetailResponse rsp = dsmService.searchDetails(requestSortBy, order, requestFilter, page, size);
		return prepareResponse(page, size, stop, rsp);
	}

	@GetMapping("/ds/aggregationhistory/{dsId}")
	public AggregationHistoryResponse aggregationHistory(@PathVariable final String dsId) throws DsmException {
		final StopWatch stop = StopWatch.createStarted();
		final List<AggregationInfo> history = wfLoggerClient.getAggregationHistory(dsId);
		final AggregationHistoryResponse rsp = new AggregationHistoryResponse(history);
		rsp.setHeader(ResponseUtils.header(history.size()));

		return prepareResponse(0, rsp.getAggregationInfo().size(), stop, rsp);
	}

	@PostMapping("/ds/searchsnippet/{page}/{size}")
	public DatasourceSnippetResponse searchSnippet(
		@RequestParam final RequestSort requestSortBy,
		@RequestParam final RequestSortOrder order,
		@RequestBody final RequestFilter requestFilter,
		@PathVariable final int page,
		@PathVariable final int size) throws DsmException {

		final StopWatch stop = StopWatch.createStarted();
		final DatasourceSnippetResponse rsp = dsmService.searchSnippet(requestSortBy, order, requestFilter, page, size);
		return prepareResponse(page, size, stop, rsp);
	}

	@PostMapping("/ds/searchregistered/{page}/{size}")
	public DatasourceSnippetResponse searchRegistered(
		@RequestParam final RequestSort requestSortBy,
		@RequestParam final RequestSortOrder order,
		@RequestBody final RequestFilter requestFilter,
		@PathVariable final int page,
		@PathVariable final int size) throws DsmException {
		final StopWatch stop = StopWatch.createStarted();

		final Page<Datasource> dsPage = dsmService.searchRegistered(requestSortBy, order, requestFilter, page, size);
		final DatasourceSnippetResponse rsp =
			ResponseUtils.snippetResponse(dsPage.map(DsmMappingUtils::asSnippetExtended).getContent(), dsPage.getTotalElements());
		return prepareResponse(page, size, stop, rsp);
	}

	@GetMapping("/ds/recentregistered/{size}")
	public SimpleResponse<RegisteredDatasourceInfo> recentRegistered(@PathVariable final int size) throws Throwable {
		final StopWatch stop = StopWatch.createStarted();
		final SimpleResponse<RegisteredDatasourceInfo> rsp = dsmService.searchRecentRegistered(size);
		return prepareResponse(1, size, stop, rsp);
	}

	@GetMapping("/ds/countregistered")
	public Long countRegistered(@RequestParam final String fromDate,
		@RequestParam(required = false) final String typologyFilter) throws Throwable {
		return dsmService.countRegisteredAfter(fromDate, typologyFilter);
	}

	@GetMapping("/ds/api/{dsId}")
	public ApiDetailsResponse getApi(
		@PathVariable final String dsId) throws DsmException {

		final StopWatch stop = StopWatch.createStarted();

		final Datasource ds = dsmService.getDs(dsId);
		final List<Api> apis = dsmService.getApis(dsId);
		final List<ApiDetails> api = apis.stream()
			.map(DsmMappingUtils::asDetails)
			.map(a -> a.setEoscDatasourceType(ds.getEoscDatasourceType()))
			.map(a -> a.setTypology(ds.getTypology()))
			.collect(Collectors.toList());
		final ApiDetailsResponse rsp = ResponseUtils.apiResponse(api, api.size());

		return prepareResponse(0, rsp.getApi().size(), stop, rsp);

	}

	@PostMapping("/api/baseurl/{page}/{size}")
	public List<String> searchBaseUrls(
		@RequestBody final RequestFilter requestFilter,
		@PathVariable final int page,
		@PathVariable final int size) throws DsmException {

		return dsmService.findApiBaseURLs(requestFilter, page, size);
	}

	@DeleteMapping("/ds/api/{apiId}")
	public void deleteApi(@PathVariable final String apiId) throws DsmForbiddenException, DsmNotFoundException {
		dsmService.deleteApi(null, apiId);
	}

	@PostMapping("/ds/manage")
	public void setManaged(
		@RequestParam final String id,
		@RequestParam final boolean managed) throws DsmException {

		log.info(String.format("updated ds '%s' managed with '%s'", id, managed));
		dsmService.setManaged(id, managed);
	}

	@GetMapping("/ds/managed/{id}")
	public boolean isManaged(@PathVariable final String id) throws DsmException {
		return dsmService.isManaged(id);
	}

	@PostMapping("/ds/add")
	public void saveDs(@Valid @RequestBody final DatasourceDetails datasource) throws DsmException {

		if (dsmService.existDs(datasource.getId())) { // TODO further check that the DS doesn't have any API
			throw new DsmException(String.format("cannot register, datasource already defined '%s'", datasource.getId()));
		}
		dsmService.saveDs(asDbEntry(datasource));
		log.info("DS saved, " + datasource.getId());
	}

	@PostMapping("/ds/addWithApis")
	public void saveDsWithApis(@Valid @RequestBody final DatasourceDetailsWithApis d) throws DsmException {
		if (d.getDatasource() == null) { throw new DsmException("Datasource field is null"); }
		if (dsmService.existDs(d.getDatasource().getId())) { // TODO further check that the DS doesn't have any API
			throw new DsmException(String.format("cannot register, datasource already defined '%s'", d.getDatasource().getId()));
		}
		dsmService.addDsAndApis(asDbEntry(d.getDatasource()), d.getApis());
	}

	@PostMapping("/ds/update")
	public void updateDatasource(@RequestBody final DatasourceDetailsUpdate d) throws DsmException, DsmNotFoundException {

		// initialize with current values from DB
		final Datasource ds = dsmService.getDs(d.getId());

		if (ds == null) { throw new DsmNotFoundException(String.format("ds '%s' does not exist", d.getId())); }

		final Datasource update = asDbEntry(d);
		if (ds.getIdentities() != null) {
			final Set<Identity> identities = new HashSet<>(
				Stream.of(update.getIdentities(), ds.getIdentities())
					.flatMap(Collection::stream)
					.collect(Collectors.toMap(i -> i.getIssuertype() + i.getPid(), Function.identity(), (i1, i2) -> i1))
					.values());
			copyNonNullProperties(update, ds);
			ds.setIdentities(identities);
		} else {
			copyNonNullProperties(update, ds);
		}

		dsmService.saveDs(ds);
	}

	@PostMapping("/ds/api/baseurl")
	@ApiResponses(value = {
		@ApiResponse(responseCode = "200", description = "OK"),
		@ApiResponse(responseCode = "500", description = "unexpected error")
	})
	public void updateBaseUrl(
		@RequestParam final String dsId,
		@RequestParam final String apiId,
		@RequestParam final String baseUrl) throws DsmException {

		log.info(String.format("updated api '%s' baseurl with '%s'", apiId, baseUrl));

		dsmService.updateApiBaseUrl(apiId, baseUrl);
	}

	@PostMapping("/ds/api/compliance")
	public void updateCompliance(
		@RequestParam final String dsId,
		@RequestParam final String apiId,
		@RequestParam final String compliance,
		@RequestParam(required = false, defaultValue = "false") final boolean override) throws DsmException {

		log.info(String.format("updated api '%s' compliance with '%s'", apiId, compliance));

		dsmService.updateCompliance(null, apiId, compliance, override);
	}

	@PostMapping("/ds/api/oaiset")
	public void updateOaiSetl(
		@RequestParam final String dsId,
		@RequestParam final String apiId,
		@RequestParam final String oaiSet) throws DsmException, DsmNotFoundException {

		dsmService.upsertApiOaiSet(apiId, oaiSet);
	}

	@PostMapping("/ds/api/add")
	public void addApi(@RequestBody final ApiDetails api) throws DsmException {
		if (StringUtils.isBlank(api.getDatasource())) { throw new DsmException("missing datasource id"); }
		if (StringUtils.isBlank(api.getId())) {
			api.setId(createId(api));
			log.info(String.format("missing api id, created '%s'", api.getId()));
		}
		dsmService.addApi(asDbEntry(api));
		log.info("API saved, id: " + api.getId());
	}

}
