package org.gcube.application.geoportal;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.gcube.application.geoportal.model.db.DBConstants;
import org.gcube.application.geoportal.model.db.DBInteractionException;
import org.gcube.application.geoportal.model.db.PostgisTable;
import org.gcube.application.geoportal.model.db.PostgisTable.Field;
import org.gcube.application.geoportal.model.db.PostgisTable.FieldType;
import org.gcube.application.geoportal.model.db.PostgisTable.GeometryType;
import org.gcube.application.geoportal.model.fault.DataParsingException;
import org.gcube.application.geoportal.model.fault.GeoPackageInteractionException;
import org.gcube.application.geoportal.storage.PostgisDBManagerI;
import org.gcube.application.geoportal.utils.CSV;
import org.gcube.application.geoportal.utils.GpkgUtils;

import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.features.user.FeatureColumn;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureResultSet;

@Slf4j
@RequiredArgsConstructor
public class PostgisTableFactory {

	@Getter
	@Setter
	@RequiredArgsConstructor
	public static class Configuration{

		public static final Configuration DEFAULT=new Configuration(false);

		@NonNull
		private Boolean skipFailingRows;
	}

	@NonNull
	private Configuration config=Configuration.DEFAULT;
	@NonNull
	private PostgisDBManagerI db;


	public PostgisTable fromCSV(String csvFile) throws DBInteractionException, FileNotFoundException, IOException, SQLException, DataParsingException {
		CSVParser parser=null;
		try {
			log.debug("Creating table from CSV FILE "+csvFile);
			parser=CSV.fromPath(csvFile);

			// Getting type 
			GeometryType type=GeometryType.POINT;			
			if(csvFile.contains("linee"))
				type=GeometryType.LINE;
			else if(csvFile.contains("poligoni"))
				type=GeometryType.POLYGON;

			PostgisTable toReturn=fromCSVHeaders(parser.getHeaderNames(), type);



			//insert Prepared statmenet
			PreparedStatement ps=db.prepareInsertStatement(toReturn, true,true);


			log.debug("Inserting records..");
			long count=0l;
			for(CSVRecord r:parser.getRecords()) {
				Map<String,String> map=r.toMap();
				try {

					toReturn.fillCSVPreparedStatament(map, ps,false);
					count+=ps.executeUpdate();
				}catch(Throwable t) {
					log.warn("Row in error : "+map);
					throw t;
				}
			}
			if(count!=parser.getRecordNumber()) 
				throw new DBInteractionException("Incoherent number of inserted records : "+count+" / "+parser.getRecordNumber());

			log.debug("Evaluating extent.. ");
			toReturn.setBoundingBox(db.evaluateBoundingBox(toReturn));
			return toReturn;


		}finally {
			if(parser!=null&&!parser.isClosed())
				parser.close();
		}
	}

	public PostgisTable getExisting(String tableName) {
		throw new RuntimeException("IMPLEMENT THIS");
	}

	public PostgisTable createAs(PostgisTable source, String tableNamePrefix) throws SQLException {
		log.debug("Creating new table as "+source);		
		PostgisTable toReturn = new PostgisTable(
				source.getTablename(),
				source.getFields(),
				source.getGeometryColumnType());
		toReturn.setTablename(tableNamePrefix+"_"+UUID.randomUUID().toString());
		
		db.create(toReturn);
		return toReturn;
		
	}
	
	public PostgisTable fromGPKGFeatureTable(GeoPackage gpkg, String featureTableName) throws SQLException, GeoPackageInteractionException, DBInteractionException, DataParsingException {
		return fromGPKGFeatureTable(gpkg, featureTableName, true);
	}

	public PostgisTable fromGPKGFeatureTable(GeoPackage gpkg, String featureTableName, boolean materialize ) throws SQLException, GeoPackageInteractionException, DBInteractionException, DataParsingException {


		List<String> tables=gpkg.getFeatureAndTileTables();
		if(!tables.contains(featureTableName)) throw new GeoPackageInteractionException("Table "+featureTableName+" not found");

		log.debug("Reading schema of "+featureTableName);

		FeatureDao featureDao = gpkg.getFeatureDao(featureTableName);
		//		log.debug("Columns : ");


		// Getting feature structure 
		List<String> featureColumnNames=new ArrayList<String>();
		List<Field> fields=new ArrayList<Field>();
		PostgisTable.GeometryType geometryType=null;


		for(FeatureColumn featCol:featureDao.getColumns()) {
			//			log.debug(featCol.getName()+"\t"+featCol.getType()+"\t"+featCol.getConstraints()+"\t"+featCol.getGeometryType());

			featureColumnNames.add(featCol.getName());
			String toSetFieldName=PostgisTable.sanitizeFieldName(featCol.getName());
			FieldType fieldType=null;

			if(featCol.getGeometryType()!=null) {
				fieldType=FieldType.GEOMETRY;


				switch(featCol.getGeometryType().toString()) {
				case "POINT" : geometryType=GeometryType.POINT;
				break;
				case "MULTIPOLYGON" : geometryType=GeometryType.POLYGON;
				break;
				case "MULTILINESTRING" : geometryType=GeometryType.LINE;
				break;
				case "MULTIPOINT" : geometryType=GeometryType.MULTIPOINT;
				break;
				default : throw new GeoPackageInteractionException("Unable to handle "+featCol.getGeometryType().toString());
				}
			}else {
				switch(featCol.getDataType()) {
				case INT:
				case INTEGER:
				case MEDIUMINT:
				case SMALLINT:
				case TINYINT : 
					fieldType=FieldType.INT;
					break;
				case DOUBLE : 
				case FLOAT :
				case REAL:
					fieldType=FieldType.FLOAT;
					break;
				default : fieldType=FieldType.TEXT;
				}
			}

			fields.add(new Field(toSetFieldName,fieldType));
		}
		String toCreate=PostgisTable.sanitizeFieldName(featureTableName+"_"+UUID.randomUUID().toString());

		PostgisTable toReturn = new PostgisTable(toCreate, fields, geometryType);

		if(materialize) {
		log.debug("Creating Table..."+toCreate);

		PreparedStatement psInsert= db.prepareInsertStatement(toReturn, true, false);


			log.debug("Filling table "+toCreate);

			FeatureResultSet rs= featureDao.query();
			long count= 0l;

			while(rs.moveToNext()) {
				try {
					Map<String,Object> row=GpkgUtils.rowAsMap(rs, fields);
					toReturn.fillObjectsPreparedStatement(row, psInsert);
					count+=psInsert.executeUpdate();
				}catch(Throwable t) {
					StringBuilder rowOutput=new StringBuilder("ROW : "); 
					for(int i=0;i<rs.getColumnCount();i++)
						rowOutput.append(featureColumnNames.get(i)+ " : "+rs.getValue(i)+ ",");

					log.warn("Unable to insert row ",t);
					if(log.isDebugEnabled())					
						log.debug("ROW was "+rowOutput);				

					if(!config.getSkipFailingRows()) throw new GeoPackageInteractionException("Error while inserting row "+rowOutput,t);
				}
			}

			long expectedCount=featureDao.count();
			if(count!=expectedCount) 
				throw new DBInteractionException("Incoherent number of inserted records : "+count+" / "+expectedCount);

			log.debug("Evaluating extent.. ");
			toReturn.setBoundingBox(db.evaluateBoundingBox(toReturn));
		}
		return toReturn;

	}




	//***************** CSV 

	private static final PostgisTable fromCSVHeaders(List<String> csvHeaders, GeometryType expectedType) {
		String tableName="l"+UUID.randomUUID().toString().toLowerCase().replaceAll("-", "_");

		//check constraints..
		switch(expectedType) {
		case POINT :{			
			if(!csvHeaders.contains(DBConstants.Defaults.XCOORD_FIELD)) throw new RuntimeException("No xcoord in headers");
			if(!csvHeaders.contains(DBConstants.Defaults.YCOORD_FIELD)) throw new RuntimeException("No ycoord in headers");
			break;
		}
		default : {
			if(!csvHeaders.contains("wkt")) 
				throw new RuntimeException("No wkt in headers");
			break;
		}		
		}

		// parsing expected field types
		ArrayList<Field> toSetFields=new ArrayList<PostgisTable.Field>();
		for(String csvHeader:csvHeaders) {
			String fieldName=PostgisTable.sanitizeFieldName(csvHeader);
			FieldType type=FieldType.TEXT;

			switch(fieldName) {		

			case(DBConstants.Defaults.XCOORD_FIELD):
			case(DBConstants.Defaults.YCOORD_FIELD):{
				type=FieldType.FLOAT;
				break;
			}
			case("year"):
			case ("fid"):
			case ("objectid"):{
				type=FieldType.INT;
				break;
			}			
			}
			toSetFields.add(new Field(fieldName, type));
		}

		GeometryType geometryColumnType=expectedType;

		toSetFields.add(new Field(DBConstants.Defaults.DEFAULT_GEOMETRY_COLUMN_NAME,FieldType.GEOMETRY));
		toSetFields.add(new Field(DBConstants.Defaults.INTERNAL_ID,FieldType.AUTOINCREMENT));


		return new PostgisTable(tableName,toSetFields, geometryColumnType);
	}




}
