
#include "database.hpp"

#include "DbConn.hpp"
#include "SqlSchema.hpp"
#include "SqlSchema_CREATE.hpp"
#include "SqlSchema_INSERT.hpp"
#include "SqlSchema_DELETE.hpp"
#include "Sql_SELECT_SurveyBounds.hpp"
#include "fitsfiles.hpp"
#include "io.hpp"

#include <iostream>
#include <iomanip>
#include <libgen.h> // basename() needed
#include <iterator>


using namespace std;

// internal

struct db_bounds
{
   unsigned int header_id;
   double lon_from;
   double lon_to;
   double lat_from;
   double lat_to;
};

vector<db_bounds> bounds_reversed(DbConn& db)
{
   LOG_trace(__func__);

   const double default_when_null = 0.0;
   struct db_bounds bounds;
   vector<db_bounds> returned_bounds;

   SqlSchema schema;
   const string lon_from{schema.eto_string(SqlSchema::lfrom)};
   const string lon_to  {schema.eto_string(SqlSchema::lto)};
   const string lat_from{schema.eto_string(SqlSchema::bfrom)};
   const string lat_to  {schema.eto_string(SqlSchema::bto)};
   const string table_boundsgalactic{schema.eto_string(SqlSchema::boundsgal)};

//--- assert LON LAT  from < to
   string sql{
      "SELECT header_id," + lon_from + "," + lon_to + "," + lat_from + "," + lat_to
         + " FROM "+ table_boundsgalactic
         + " WHERE ("
         + lon_from + " > " + lon_to
         + ") or ("
         + lat_from + " > " + lat_to + ")"};

//---- assert LON LAT are normalized
   string sql2{
      "SELECT header_id," + lon_from + "," + lon_to + "," + lat_from + "," + lat_to
         + " FROM "+ table_boundsgalactic
         + " WHERE ("
         + lon_from + " > " + lon_to
         + ") or ("
           "(" + lon_from + " <  0) and ((" + lon_from + " < -180) or (" + lon_to + " >= 180 ))"
           ") or ("
           "(" + lon_from + " >= 0) and ((" + lon_from + " >= 360) or (" + lon_to + " >= 360))"
           ") or ("
         + lat_from + " > " + lat_to
         + ") or ("
           "(" + lat_from + " < -90 ) or (" + lat_from + " >= 90) or "
           "(" + lat_to   + " < -90 ) or (" + lat_to   + " >= 90)"
           ")"};
//----


   pqxx::result pqxx_result{db.dbExecQuery3(sql2)};

   try
   {
      for (auto row: pqxx_result)
      {
         // FIXME take col-names SqlSchema_SELECT_bouns
         bounds.header_id = row["header_id"].as<unsigned int>();
         bounds.lon_from  = row[lon_from].as<double>(default_when_null);
         bounds.lon_to    = row[lon_to].as<double>(default_when_null);
         bounds.lat_from  = row[lat_from].as<double>(default_when_null);
         bounds.lat_to    = row[lat_to].as<double>(default_when_null);

         returned_bounds.push_back(bounds);
      }
   }
   catch (std::exception const &e) 
   {
      LOG_STREAM << "ERR " + string(__FILE__) + "." + to_string(__LINE__) + ": " << e.what() << endl;
   }

   return returned_bounds;
}


// external

//----------------------------------------------------------------------//
// init db:
// all tables will be deleted if existed
// create surveys table and load its values from config::SurveysFile
// create other tables empty
//----------------------------------------------------------------------//
void database::dbInit(const string db_uri, const string db_schema, const string surveys_csv_pathname)
{
   LOG_trace(__func__);

   DbConn db(db_uri, db_schema);

   Survey tabSurveys("surveys");

   SqlSchema_CREATE cmdCreate;

   vector<string> createAndLoadSurveys = { 
      tabSurveys.getCREATE(),
      tabSurveys.getINSERT(surveys_csv_pathname)
   };

   vector<string> createHeadersDb(cmdCreate.getCREATE());

   db.dbExecCmds(createAndLoadSurveys);
   db.dbExecCmds(createHeadersDb);
};



//----------------------------------------------------------------------//
// check - verify consistency of database 
// RULE1: each table must contain the same number of rows
//----------------------------------------------------------------------//
vector<string> database::dbCheck(const string db_uri, const string db_schema)
{
   DbConn db(db_uri, db_schema);

   vector<string> result_strings;

   // check 1: cube/image bounds in right order: from <= to

/*  vector<db_bounds> reversed_bounds = bounds_reversed(db);

   if(reversed_bounds.size() > 0) result_strings.push_back("assert from < to  failed in these rows:");
   for(db_bounds one_row : reversed_bounds)
   {
      result_strings.push_back(
            to_string(one_row.header_id)
            + ": (" + to_string(one_row.lon_from)
            + ", " + to_string(one_row.lon_to)
            + ")   (" + to_string(one_row.lat_from)
            + ", " + to_string(one_row.lat_to)
            + ")"
            );
   }
*/
   // check 2: rows count equal in all tables

   vector<string> tables = db.queryTableNames();

   string qRowsCount = "SELECT ";
   for(const string &table : tables)
      qRowsCount += "(SELECT count(*) FROM " + table + ") as " + table + ",";
   qRowsCount.erase(qRowsCount.size()-1);// remove last coma

   vector<string> table_sizes{db.dbExecQuery2(qRowsCount)};

   result_strings.insert(
         result_strings.end(),
         std::make_move_iterator(table_sizes.begin()),
         std::make_move_iterator(table_sizes.end())
         );

   return result_strings;
};









//----------------------------------------------------------------------//
// query Surveys Table for defined surveys
// and list how many entries (FITS-filse) each Survey has in the system
// (Headers, ObsCore, VerticesGalactic,... tables)
// Side-effect: check that number of entries in different tables is equal, otherwise
// tables are inconsistent; DB is in error state
//----------------------------------------------------------------------//
vector<string> database::dbListSurveys(const string db_uri, const string db_schema)
{
   LOG_trace(__func__);

   DbConn db(db_uri, db_schema);

   string qSidNameCount(
         "SELECT survey_id,count(headers.survey_id), name,species,transition " 
         "FROM headers "
         // only common rows         "INNER JOIN surveys USING (survey_id) "
         "RIGHT OUTER JOIN surveys USING (survey_id) " // all rows of surveys table
         "GROUP BY survey_id,surveys.name,surveys.species,surveys.transition "
         "ORDER BY surveys.survey_id");

   vector<string> res_str;// = db.dbExecQuery2(qSidNameCount);
   //pqxx::result = res = db.dbExecQuery3(qSidNameCount);
   return res_str;
};




void database::dbSurveyBounds(/*int sid,*/ const string db_uri, const string db_schema)
{
   LOG_trace(__func__);

   DbConn db(db_uri, db_schema);

   // FIXME re-implement with obscore::s_region or polygon_region(_galactic)
   //
   //db.dbExecQuery(Sql_SELECT_SurveyBounds::qLonMinMax(sid));
   //db.dbExecQuery(Sql_SELECT_SurveyBounds::qLatMinMax(sid));
}




string database::dbListFiles(int sid, const string db_uri, const string db_schema, const string fitsdir)
{
   LOG_trace(__func__);

   DbConn db(db_uri, db_schema);

   Survey surv = db.querySurveyAttributes(sid);
   string storageFilter = surv.getStorageFilter();

   std::vector<string> pathnames =
      fitsfiles::globVector(fitsdir + "/" + storageFilter);

//   LOG_STREAM << "SID[" << setw(3) <<sid << "]: " << setw(5) <<pathnames.size() << "    "
//      << surv.survName << " " << surv.survSpecies << " " << surv.survTransition << endl;

   return "SID[" + to_string(sid) + "]: " + to_string(pathnames.size()) + "    "
      + surv.survName + " | " + surv.survSpecies + " | " + surv.survTransition;
}



/* FIXME implement add-survey optionally without need of csv-metadata with params:
 * const string storage_filter, // identify files
 * const int    calib_level,    // must provide calib-level for them
 * const string obs_collection, // give it a name
 */
void database::dbAddSurvey(int sid, const string groups,
      const string obscore_publisher,
      const string obscore_access_format,
      const string obscore_access_url,
      const string db_uri, const string db_schema, const string fitsdir,
		int max_hdupos, int min_hdupos)
{
   LOG_trace(__func__);

   // 1, load optional metadata (from Db or csv-file) if exist
   DbConn db(db_uri, db_schema); 
   Survey surv = db.querySurveyAttributes(sid);


   // 2, storage-filter identifies the files: collect them
   string storageFilter = surv.getStorageFilter();

	LOG_STREAM << "StorageFilter: " << storageFilter << endl;

   std::vector<string> pathnames = fitsfiles::globVector(fitsdir + "/" + storageFilter);
   if(pathnames.size() == 0)
   {
      LOG_STREAM << "Found " << pathnames.size() << " files in " << fitsdir + "/" + storageFilter << endl;
      return;
   }


   // 3,a, set obscore mandatory fields: collection-level (the same for all files in the collection)
   const int calib_level       = surv.calibLevel;
   const string obs_collection = surv.getObsCollection();

   // generate 'INSERT INTO obscore ...' SQL-statement
   const fitsfiles::keys_by_type in_keys
   {
      ObsCoreKeys::add_str_keys(
            ObsCoreKeys::strKeys, std::set<std::string>{surv.fitskeyFacilityName, surv.fitskeyInstrumentName}),
         ObsCoreKeys::uintKeys,
         ObsCoreKeys::doubleKeys
   };
   SqlSchema_INSERT cmdInsert;
   const string access_format{obscore_access_format};

	LOG_STREAM << "FITS files found: " << to_string(pathnames.size()) << endl;

   for(unsigned int i=0; i < pathnames.size(); i++)
   {
      // do optional obscore::access_* fields
      string pathname = pathnames.at(i);
      const uintmax_t access_estsize{fitsfiles::fileSize(pathname)/1024};
      // FIXME fitsfiles::globVector() should return rel-pathnames already
      string c_pathname = pathname;
      string filename = basename((char*)c_pathname.c_str());
      string rel_pathname{surv.storagePath + "/" + filename};
      const string access_url{obscore_access_url.empty()
         ?  nullptr : obscore_access_url + "/" + rel_pathname};


      // 3,b, set obscore mandatory fields: file-dependent
      const string obs_id            = rel_pathname.substr(0, rel_pathname.find_last_of("."));
      const string obs_publisher_did = obscore_publisher + "?" + rel_pathname;

      // optional
      const string obs_title{surv.getObsTitle()};

      // 4. set optional values which are available (in header or in metadata)
      try
      {
         const std::vector<fitsfiles::Hdu> all_hdu{fitsfiles::fname2hdrstr(pathname, max_hdupos, min_hdupos, &in_keys)};

			//LOG_STREAM << "HDU-count: " << to_string(all_hdu.size()) << endl;

         for(fitsfiles::Hdu hdu : all_hdu)
         {
            cmdInsert.appendRow(
                  calib_level,
                  obs_collection,
                  obs_id,
                  obs_publisher_did,
                  obs_title,
                  surv.survDescription,
                  surv.dataproductType,
                  surv.oUcd,
                  access_url, access_format, access_estsize,
                  hdu,
                  surv.restFrequency,
                  surv.authPolicy,
                  groups);
         }
      }
      catch (std::exception const &e)
      {
         LOG_STREAM << "ERR " + string(__func__) + ": " << pathname << " : "<< e.what() << endl;
      }
   }

   db.dbExecCmds(cmdInsert.getINSERT());

}


void database::dbModifyGroups(int sid, const string groups,
      const string obscore_publisher,
      const string db_uri, const string db_schema)
{
   LOG_trace(__func__);

   DbConn db(db_uri, db_schema); 
   Survey surv = db.querySurveyAttributes(sid);

	vector<string> cmdModGroups
	{
		"UPDATE obscore SET groups = ARRAY["+ groups +"]::TEXT[] WHERE (policy = 'PRIV') AND (obs_collection = '"+ surv.getObsCollection() +"') AND (obs_title = '"+ surv.getObsTitle()  +"')"
	};

	db.dbExecCmds(cmdModGroups);
}



// FIXME hid identifies rows to remove in tables -> no good, should be pubdid as identificator
void database::dbRemoveSurvey(int sid, const string db_uri, const string db_schema)
{
	LOG_trace(__func__);

	DbConn db(db_uri, db_schema);

	Survey surv = db.querySurveyAttributes(sid);

	SqlSchema_DELETE cmdDelete;

	vector<string> deleteRowsAllTables = {cmdDelete.getCommand(sid, surv)};

	db.dbExecCmds(deleteRowsAllTables);
}




