/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.defaults.data;


import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.problem.ProblemFactory;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;

import org.geotools.data.wfs.WFSDataStore;
import org.geotools.data.wfs.WFSDataStoreFactory;
import org.geotools.data.wfs.internal.Versions;
import org.geotools.data.wfs.internal.WFSGetCapabilities;
import org.geotools.util.Version;
import org.geotools.api.filter.Filter;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;

import net.opengis.ows11.DomainType;
import net.opengis.ows11.OperationType;
import net.opengis.wfs20.WFSCapabilitiesType;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.FeatureSources;
import nz.org.riskscape.engine.auth.HttpSecret;
import nz.org.riskscape.engine.auth.HttpSecret.Request;
import nz.org.riskscape.engine.bind.ParameterField;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.relation.FeatureSourceBookmarkResolver;
import nz.org.riskscape.engine.projection.ForceSridProjection;
import nz.org.riskscape.engine.projection.LinearGeometryProjection;
import nz.org.riskscape.engine.relation.FeatureSourceRelation;
import nz.org.riskscape.engine.relation.PagingFeatureSourceRelation;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.problem.ResultOrProblems;

public class WfsBookmarkResolver extends FeatureSourceBookmarkResolver<WFSDataStore, WfsBookmarkResolver.WfsParams> {

  public static class WfsParams extends FeatureSourceBookmarkParams {
    @ParameterField
    public int wfsPageSize = DEFAULT_WFS_PAGE_SIZE;
  }

  // WFS is designed to efficiently stream large data sets, but it assumes the client can't do the same thing, so
  // tends to limit the number of features it will return.  So we pick a number as a page size in the hope that
  // the WFS server won't restrict the size of the returned data set
  private static final int DEFAULT_WFS_PAGE_SIZE = 100000;

  public interface LocalProblems extends ProblemFactory {
    Problem badWfsTarget(URI target);
  }
  public static final LocalProblems PROBLEMS = Problems.get(LocalProblems.class);

  public WfsBookmarkResolver(Engine engine) {
    super(engine);
  }

  private final WFSDataStoreFactory dataStoreFactory = new WFSDataStoreFactory();

  @Override
  protected WFSDataStore createDataStore(WfsParams params) throws MalformedURLException, IOException {
    Request request = HttpSecret.getRequest(params.getLocation(), engine);
    Map<String, ?> dsParams = ImmutableMap.of(
        WFSDataStoreFactory.TIMEOUT.key, FeatureSources.WFS_TIMEOUT_MS,
        WFSDataStoreFactory.URL.key, request.getURI().toURL(),
        WFSDataStoreFactory.ADDITIONAL_HEADERS.key, request.getHeaders().stream()
            .collect(Collectors.toMap(p -> p.getLeft(), p -> p.getRight()))
    );
    try {
      return dataStoreFactory.createDataStore(dsParams);
    } catch(IOException ex) {
      throw new RiskscapeException(
        PROBLEMS.badWfsTarget(params.getLocation()).withChildren(Problems.caught(ex))
      );
    }
  }

  @Override
  protected ResultOrProblems<Relation> wrapRelation(
      WfsParams params,
      WFSDataStore dataStore,
      FeatureSourceRelation relation,
      CoordinateReferenceSystem crs) {

    ResultOrProblems<Relation> wrapped;
    String versionString = dataStore.getInfo().getVersion();
    if (versionString != null && new Version(versionString).equals(Versions.v2_0_0)) {
        wrapped = ResultOrProblems.of(
            new PagingFeatureSourceRelation(
                relation.getType(),
                relation.getFeatureSource(),
                relation.getSridSet(),
                Filter.INCLUDE,
                crs,
                getPageSize(dataStore.getWfsClient().getCapabilities(), params.wfsPageSize)
            ));
    } else {
      wrapped = ResultOrProblems.of(relation);
    }

    // add in the LinearGeometryProjection before we add in the force srid projection
    // otherwise force srid projection will convert any curved geometries to geometry collections.
    wrapped = wrapped.flatMap(r -> r.project(new LinearGeometryProjection(params.linearizeTolerance)));

    if (! params.crs.isPresent()) {
      // The WFS data store will make it's own geometry factory, completely ignoring the geometry factory
      // hint that we give it. This results in all the geometries having SRID: 0.
      // We can project the relation with ForceSridProjection to ensure the geometries get the correct SRID assigned.
      // But we don't need to do this is the user has set crs-name as RelationBookmarkResolver will take
      // care of it.
      return wrapped.flatMap(rel -> rel.project(new ForceSridProjection(crs, relation.getSridSet())));
    }
    return wrapped;
  }

  /**
   * Returns the appropriate page size to use for the given capabilities and user specified page size.
   *
   * The returned page size will be the smaller of the servers page size (if one exists) or the user
   * specified size.
   *
   * Package level access to test use.
   *
   * @param serverCapabilities
   * @param userSpecifiedPageSize
   * @return
   */
  int getPageSize(WFSGetCapabilities serverCapabilities, int userSpecifiedPageSize) {
    // WFS servers advertise their page size in an ows element named CountDefault.
    Integer serverPageSize = null;
    if (serverCapabilities.getParsedCapabilities() instanceof WFSCapabilitiesType) {
      WFSCapabilitiesType wfsCapabilities = (WFSCapabilitiesType) serverCapabilities.getParsedCapabilities();

      // The OperationsMetadata API is missing generics making life a little messy
      @SuppressWarnings("unchecked")
      List<OperationType> operationTypes = wfsCapabilities.getOperationsMetadata().getOperation();
      for (OperationType operationType: operationTypes) {
        if ("GetFeature".equals(operationType.getName())) {
          @SuppressWarnings("unchecked")
          List<DomainType> constraints = operationType.getConstraint();
          serverPageSize = getCountDefaultConstraint(constraints);
          break;
        }
      }
      if (serverPageSize == null) {
        // there is no CountDefault in the GetFeature operation. maybe there is one in operations metadata
        // which will apply to all operations
        @SuppressWarnings("unchecked")
        List<DomainType> constraints = wfsCapabilities.getOperationsMetadata().getConstraint();
        serverPageSize = getCountDefaultConstraint(constraints);
      }
    }

    if (serverPageSize == null || serverPageSize > userSpecifiedPageSize) {
      // there is no server page size or it is larger then the users limit. go with the user setting.
      // note we ignore the user setting when it is larger because PagingFeatureSourceRelation would
      // not know if the results are complete or not in that case.
      return userSpecifiedPageSize;
    }
    // server pageSize wins
    return serverPageSize;
  }

  private Integer getCountDefaultConstraint(List<DomainType> constraints) {
    for (DomainType constraint: constraints) {
      if ("CountDefault".equals(constraint.getName())) {
        try {
          int countDefault = Integer.parseInt(constraint.getDefaultValue().getValue());
          // the WFS spec says the CountDefault must be >= 0, but zero would be a dumb page-size
          if (countDefault > 0) {
            return countDefault;
          }
        } catch(NumberFormatException e) {}
      }
    }
    return null;
  }

  @Override
  public String getFormat(Bookmark bookmark) {
    String format = super.getFormat(bookmark);

    if (Strings.isNullOrEmpty(format)) {
      URI location = bookmark.getLocation();
      String scheme = location.getScheme();
      String path = location.getPath();
      if (scheme != null && scheme.startsWith("http") && path != null && path.toLowerCase().contains("/wfs")) {
         return "wfs";
      }
    }

    return format;
  }

  @Override
  public Set<String> getFormats() {
    return Collections.singleton("wfs");
  }

  @Override
  protected Map<String, String> getExtensionsToFormats() {
    return Collections.emptyMap();
  }

}
