/*
 * 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.data.relation;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import nz.org.riskscape.engine.FileProblems;

import org.geotools.api.data.DataStore;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.referencing.crs.DefaultEngineeringCRS;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;

import com.google.common.base.Strings;

import lombok.extern.slf4j.Slf4j;

import nz.org.riskscape.ReflectionUtils;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.JavaParameterSet;
import nz.org.riskscape.engine.bind.ParameterField;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.BookmarkProblems;
import nz.org.riskscape.engine.data.relation.FeatureSourceBookmarkResolver.FeatureSourceBookmarkParams;
import nz.org.riskscape.engine.problem.GeneralProblems;
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.Relation;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * Provides a basic framework for constructing a {@link FeatureSourceRelation} from the various geotools APIs
 */
@Slf4j
public abstract class
FeatureSourceBookmarkResolver<DS extends DataStore, BP extends FeatureSourceBookmarkParams>
  extends RelationBookmarkResolver<BP> {

  public static class FeatureSourceBookmarkParams extends RelationBookmarkParams {
    @ParameterField
    public Optional<Double> linearizeTolerance = Optional.empty();

  }

  // https://gitlab.catalyst.net.nz/riskscape/riskscape/-/issues/307 - we end up passing other feature formats through
  // this code path as well, doesn't seem to hurt or be worth adding complexity to avoid it (yet)
  public static final LockDefeater LOCK_DEFEATER = new LockDefeater("lock-defeater");

  /**
   * If set the named layer will be used for this bookmark, else the first layer from the data source is used.
   */
  public static final String DATA_LAYER = "layer";

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

  /**
   * Wrap the {@link FeatureSourceSourceRelation} to make it's content compatible with the broader RiskScape
   * system.
   *
   * At the least this will should include the {@link LinearGeometryProjection} to ensure curved geometries
   * are not emitted.
   *
   * Subclasses may also need to add in the {@link ForceSridProjection} if the feature source does not make
   * use of the correct geometry factory (thus not setting a RiskScape compatible SRID).
   */
  protected ResultOrProblems<Relation> wrapRelation(
      BP params,
      DS dataStore,
      FeatureSourceRelation relation,
      CoordinateReferenceSystem crs) {
    // some datasources may included curved geometries that are not supported by riskscape. we deal
    // with these by converting them to linear geometries with the LinearGeometryProjection.
    // this projection needs to be very close to the data source to do it's job. specifically it must
    // be before any ForceSridProjection which will convert curved geometries to a geometry collection.
    return relation.project(new LinearGeometryProjection(params.linearizeTolerance));
  }

  @Override
  protected ResultOrProblems<Relation> createRawRelationFromBookmark(BP params) {

    DS dataStore;

    try {
      dataStore = LOCK_DEFEATER.call("create-datastore", () -> createDataStore(params));
    } catch (Exception e) {
      return ResultOrProblems.failed(Problems.caught(e));
    }

    // sanity-check in case we've been given a garbage file, in which case geotools will fall over reading it
    Problems problems = checkDataValid(params, dataStore);
    if (problems != null) {
      File file = getBookmarkedPath(params).toFile();
      return ResultOrProblems.failed(
          FileProblems.get().geotoolsCannotRead(file).withChildren(problems),
          FileProblems.get().badGeospatialDataTip(file, getFormat(params.bookmark))
        );
    }

    ResultOrProblems<SimpleFeatureSource> featureSource = createFeatureSource(params, dataStore);

    return featureSource.flatMap(sfs -> {
      CoordinateReferenceSystem crs = crsAtAllCosts(params, sfs);
      return wrapRelation(params,
          dataStore,
          new FeatureSourceRelation(sfs, params.getProject().getSridSet(), crs),
          crs
      );
    });
  }

  public CoordinateReferenceSystem crsAtAllCosts(BP params, SimpleFeatureSource sfs) {
    if (params.crs.isPresent()) {
      return params.crs.get();
    }

    CoordinateReferenceSystem crs = LOCK_DEFEATER.call("get-crs", ()
        -> sfs.getSchema().getGeometryDescriptor().getCoordinateReferenceSystem());

    if (crs == null) {
      // this might be because the prj file is in a format that geotools can't read, but maybe gdal can...
      crs = attemptPrjParse(params.getLocation());
    }

    if (crs == null) {
      log.warn("No crs could be parsed for feature source from {}, falling back to generic 2d", params.getLocation());
      crs = DefaultEngineeringCRS.GENERIC_2D;
    }

    return crs;
  }

  /**
   * @param params
   * @return a {@link DataStore} that can return the right kind of {@link SimpleFeatureSource} for the given
   * {@link Bookmark}
   * @throws IOException
   * @throws MalformedURLException
   */
  protected abstract DS createDataStore(BP params) throws MalformedURLException, IOException;

  /**
   * @return a {@link SimpleFeatureSource} from the given {@link DataStore} according to the given {@link Bookmark}
   */
  protected ResultOrProblems<SimpleFeatureSource> createFeatureSource(BP params, DS dataStore) {

      String layer = params.layer.orElse(null);
      List<String> availableLayers;
      List<Problem> warnings;
      try {
        availableLayers = Arrays.asList(dataStore.getTypeNames());

        if (availableLayers.isEmpty()) {
          // there are no feature layers in the dataStore so we return a specific failure for this.
          return ResultOrProblems.failed(BookmarkProblems.get().noFeatureLayers());
        }
        if (Strings.isNullOrEmpty(layer)) {
          layer = availableLayers.get(0);
          if (availableLayers.size() > 1) {
            warnings = Arrays.asList(Problem.warning(
                "DataStore '%s' contains multiple layers but no layer parameter given.  Selecting '%s'",
                params.getLocation(), layer));
          } else {
            warnings = Collections.emptyList();
          }
        } else {
          warnings = Collections.emptyList();
        }

        if (!availableLayers.contains(layer)) {
          return ResultOrProblems.failed(Problem.error("Requested layer '%s' does not exist.  Available: %s", layer,
              availableLayers));
        } else {
          String finalLayer = layer;
          return LOCK_DEFEATER.call("get-feature-source", () -> {
            try {
              return ResultOrProblems.of(dataStore.getFeatureSource(finalLayer), warnings);
            } catch (IOException e) {
              return ResultOrProblems.failed(Problem.error(e, "I/O Error accessing layer - %s", e.getMessage()));
            }
          });
        }
      } catch (IOException e) {
        return ResultOrProblems.failed(Problem.error(e, "I/O Error accessing layer - %s", e.getMessage()));
      }
  }

  @Override
  protected JavaParameterSet<BP> buildParameterSet() {
    return JavaParameterSet.fromBindingClass(ReflectionUtils.findParameterClass(getClass(), 1));
  }

  /**
   * Sanity-check we can read the underlying data source using
   */
  protected Problems checkDataValid(BP params, DS dataStore) {
    // this will vary per implementation, as the underlying geotools code will explode at different points
    return null;
  }

  @Override
  protected void validateParameters(BP params, BindingContext context) {
    super.validateParameters(params, context);

    if (params.linearizeTolerance.isPresent() && params.linearizeTolerance.get() < 0D) {
      params.problems.add(
          GeneralProblems.get().badValue(params.linearizeTolerance.get(), "linearize-tolerance", ">= 0")
      );
    }
  }

}
