
#include "database.hpp"
#include "fitsfiles.hpp"
#include "config.hpp"
#include "io.hpp"

#include <iostream>
#include <sstream>

#include <assert.h>


std::ostream& OUT_STREAM = std::cout;
std::ostream& ERROR_STREAM = std::cerr;


using namespace std;

namespace vlkb
{
   const int EXIT_WITH_USAGE = -1000;
   const int EXIT_WITH_ERROR = -2000;

   config conf;

   void usage(const std::string progname)
   {
      std::cout 
         << "Usage: " << progname << " <conf_filename> <command> [cmd-options] [cmd-args]" << endl
         << "\n where conf_filename is typically /etc/" << progname << "/datasets.conf\n "
         << "\n and where commands are:\n "
         << "\n\t dbfiles dbinit dbcheck dblist dbadd dbmodgroups dbremove\n"
         << std::endl
         << "Version: " << VERSIONSTR << " " << BUILD << std::endl;
   }



   // program recognizes following commands

   enum cmd_set {dbinit, dbcheck, dblist, dbfiles, dbaddsurvey, dbmodgroups, dbremovesurvey};


   // from bash receive args as strings

   cmd_set to_cmd(const std::string cmdstr)
   {
      cmd_set cmd;

      if(cmdstr.compare("dbinit") == 0)       cmd = dbinit;
      else if(cmdstr.compare("dbcheck") == 0)      cmd = dbcheck;
      else if(cmdstr.compare("dblist") == 0)       cmd = dblist;
      else if(cmdstr.compare("dbfiles") == 0)      cmd = dbfiles;
      else if(cmdstr.compare("dbadd") == 0)        cmd = dbaddsurvey;
      else if(cmdstr.compare("dbmodgroups") == 0)  cmd = dbmodgroups;
      else if(cmdstr.compare("dbremove") == 0)     cmd = dbremovesurvey;
      else
      {
         stringstream cmd_err;
         cmd_err << "String \'" << cmdstr<< "\' not recognized as valid command." << endl;
         throw(invalid_argument(cmd_err.str()));
      }

      return cmd;
   }

} // namespce


std::string base_name(std::string path)
{
   return path.substr(path.find_last_of("//") + 1);
   // FIXME replace with basename
}



//-----------------------------------------------------------
// all commands handle [argc,argv] and return a code:
// 0 - Ok: command produces result as described in 'usage'
// non-zero: Warning, Error: response has other form that 
// the one described in 'usage'
//-----------------------------------------------------------


int cmd_dbInit(int argc, char * argv[])
{
   int rc;
   try
   {
      if(argc!=2)
      {
         std::cout << "Usage: dbinit <surveys.csv>" << endl;
         return vlkb::EXIT_WITH_USAGE;
      }

      const string csv_filename{argv[1]};

      OUT_STREAM << "DB: " << vlkb::conf.getDbUri() << " schema["<< vlkb::conf.getDbSchema() << "]" << endl;
      OUT_STREAM << "This will delete all data in database, confirm with 'yes':" << endl;
      string answer;
      std::cin >> answer;
      if(answer.compare("yes") == 0) 
      {
         database::dbInit(vlkb::conf.getDbUri(), vlkb::conf.getDbSchema(), csv_filename);
         rc = 0;
      }
      else
      {
         rc = vlkb::EXIT_WITH_ERROR;
      }
   }
   catch(exception& e)
   {
      rc = vlkb::EXIT_WITH_ERROR;
      ERROR_STREAM << e.what() << endl;
   }
   return rc;
}



int cmd_dbCheck(void)
{
   int rc;
   try
   {
      vector<string> db_rows{database::dbCheck(vlkb::conf.getDbUri(), vlkb::conf.getDbSchema())};
      for(string one_row : db_rows) cout << one_row << endl;
      rc = 0;
   }
   catch(exception& e)
   {
      rc = vlkb::EXIT_WITH_ERROR;
      ERROR_STREAM << e.what() << endl;
   }
   return rc;
}



int cmd_dbList(void)
{
   int rc;
   try
   {
      vector<string> res_str = database::dbListSurveys(vlkb::conf.getDbUri(), vlkb::conf.getDbSchema());
      rc = 0;
      for(string res_row : res_str)
         cout << res_row << endl;
   }
   catch(exception& e)
   {
      rc = vlkb::EXIT_WITH_ERROR;
      ERROR_STREAM << e.what() << endl;
   }
   return rc;
}


int cmd_dbFiles(int argc, char * argv[])
{
   int rc;
   switch(argc)
   {
      case 2:
         try
         {
            string row = database::dbListFiles(std::stoi(argv[1]), vlkb::conf.getDbUri(), vlkb::conf.getDbSchema(), vlkb::conf.getFitsDir());
            rc = 0;
            cout << row << endl;
         }
         catch(exception& e)
         {
            rc = vlkb::EXIT_WITH_ERROR;
            ERROR_STREAM << e.what() << endl;
         }
         break;

      case 3:
         try
         {
            for(int i=std::stoi(argv[1]); i<=std::stoi(argv[2]); i++)
               cout << 
                  database::dbListFiles(i, vlkb::conf.getDbUri(), vlkb::conf.getDbSchema(), vlkb::conf.getFitsDir())
                  << endl;
            rc = 0;
         }
         catch(exception& e)
         {
            rc = vlkb::EXIT_WITH_ERROR;
            ERROR_STREAM << e.what() << endl;
         }
         break;

      default:
         cout << "Usage: dbfiles <SID-from> [SID-to]" << endl
            << "prints file-count for given survey(s)" << endl;
         rc = vlkb::EXIT_WITH_USAGE;
   }
   return rc;
}



int cmd_dbAdd(int argc, char * argv[])
{
   const string usage_str{"Usage: dbadd <SID> [SID_to]\n"
              	           "       dbadd <SID> [--extnum N]\n"
             	           "inserts survey metadata to database\n"
             	           "works on Primary HDU unless 'extnum' given:\n"
             	           "--extnum N where N=1..n (1 is the first extension after the Primary HDU)"};
 
   int max_hdupos = 1;
   int min_hdupos = 1;
   const string groups = ""; // NOTE: uses cmd 'dbmodgroups' to add groups
   int sid_from, sid_to;
   int rc = 0;

   switch(argc)
   {
      case 2:
         sid_from = std::stoi(argv[1]);
         sid_to   = sid_from;
         break;

      case 3:
         sid_from = std::stoi(argv[1]);
        	sid_to   = std::stoi(argv[2]);
         break;

      case 4:
         sid_from = std::stoi(argv[1]);
         sid_to   = sid_from;
			if(0 == string{"--extnum"}.compare(string{argv[2]}))
			{
         	min_hdupos = max_hdupos = 1 + std::stoi(argv[3]);
			}
			else
			{
				cout << "--extnum expected\n" << usage_str << endl;
         	return vlkb::EXIT_WITH_USAGE;
			}
         break;

      default:
			cout << usage_str << endl;
         return vlkb::EXIT_WITH_USAGE;
   }

   OUT_STREAM << "This will add all data into database. Confirm with 'yes':" << endl;
   string answer;
   std::cin >> answer;
   if(answer.compare("yes") != 0) return vlkb::EXIT_WITH_ERROR; 

   for(int i=sid_from; i<=sid_to; i++)
   {
      try
      {
         database::dbAddSurvey(i, groups,
               vlkb::conf.getObsCorePublisher(), vlkb::conf.getObsCoreAccessFormat(), vlkb::conf.getObscoreAccessUrl(),
               vlkb::conf.getDbUri(), vlkb::conf.getDbSchema(), vlkb::conf.getFitsDir(), max_hdupos, min_hdupos);
      }
      catch(exception& e)
      {
         rc = vlkb::EXIT_WITH_ERROR;
         ERROR_STREAM << "SID["<< i << "]: " << e.what() << endl;
      }
   }

   return rc;
}

// convert argv[] bash arg to sql-TEXT[] string

// segment fault if str is empty or contains _only_ delimiter(s): "," ",," ",,,,,,,,"
std::vector<std::string> split_by_delimiter(std::string_view str, std::string_view delimiter)
{
   if (delimiter.empty()) return {std::string(str)};
   // handle empty delimiters explicitly so we can't fall into an infinite loop

   std::vector<std::string> tokens;
   std::size_t              cursor        = 0;
   std::size_t              segment_start = cursor;

   while ((cursor = str.find(delimiter, cursor)) != std::string_view::npos)
   {
      if (segment_start != cursor) tokens.emplace_back(str.substr(segment_start, cursor - segment_start));
      // don't emplace empty tokens in case of leading/trailing/repeated delimiters
      cursor += delimiter.size();
      segment_start = cursor;
   }

   if (segment_start != str.size()) tokens.emplace_back(str.substr(segment_start));
   // 'cursor' is now at 'npos', so we compare to the size instead

   return tokens;
}

inline std::string join_strings(std::vector<std::string> arr, std::string sep, std::string wrap)
{
   std::string out = wrap + arr[0] + wrap;
   for(unsigned int i = 1; i < arr.size(); i++) {
      out += sep + wrap + arr[i] + wrap;
   }
   return out;
}

const string EMPTY_STRING;
string to_sql_text_vec(string arg)
{
	if(!arg.empty())
	{
		vector<string> arg_split = split_by_delimiter(arg, string{','} );
		if(!arg_split.empty())
		{
			//for (const auto& elem: arg_split) cout << elem << ' ';
			//cout << '<' << endl;

			string sql_text_arr{join_strings(arg_split,",","'")};
			OUT_STREAM << "SQL ARRAY[" << sql_text_arr << "]"<< endl;
			return sql_text_arr;
		}
	}
	OUT_STREAM << "Cleared all groups for the given datasets." << endl;
	return EMPTY_STRING;
}

int cmd_dbModGroups(int argc, char * argv[])
{
	string groups;
	int sid_from, sid_to;
	int rc;
	switch(argc)
	{
		case 3:
			groups   = std::string(argv[1]);
			sid_from = std::stoi(argv[2]);
			sid_to   = sid_from;
			break;

		case 4:
			groups   = std::string(argv[1]);
			sid_from = std::stoi(argv[2]);
			sid_to   = std::stoi(argv[3]);
			break;

		default:
			cout << endl
				  << "Modifies list of groups which are allowed to access proprietary datasets given by SID." << endl
				  << "SID (survey-id) is in the metadata file in the root of FITS-file store (typically /srv/datasets/<metadata>.csv)."
				  << "Empty groups (\"\") arg clears the groups list in the database." << endl
				  << "Usage:" << endl
				  << "   dbmodgroups <groups> <SID-from> [SID-to]" << endl
              << endl
				  << "Examples (groups are comma separated):" << endl
				  << "   vlkb-obscore datasets.conf dbmodgroups \"Group A,Group B\" 3 6" << endl
				  << "   vlkb-obscore datasets.conf dbmodgroups \"\" 3 6" << endl;
			rc = vlkb::EXIT_WITH_USAGE;
			return rc;
	}

	try
	{
		string sql_groups_vec{to_sql_text_vec(groups)};

		for(int i=sid_from; i<=sid_to; i++)
			database::dbModifyGroups(i, sql_groups_vec, vlkb::conf.getDbUri(), vlkb::conf.getDbSchema());
		rc = 0;
	}
	catch(exception& e)
	{
		rc = vlkb::EXIT_WITH_ERROR;
		ERROR_STREAM << e.what() << endl;
	}

	return rc;
}



int cmd_dbRemove(int argc, char * argv[])
{
	int rc;
	switch(argc)
	{
		case 2:
			try
			{
				database::dbRemoveSurvey(stoi(argv[1]), vlkb::conf.getDbUri(), vlkb::conf.getDbSchema());
				rc = 0;
			}
			catch(exception& e)
			{
				rc = vlkb::EXIT_WITH_ERROR;
				ERROR_STREAM << e.what() << endl;
			}
			break;

		default:
			cout << "Usage: dbremove <SID>" << endl
				<< "removes survey metadata from database" << endl;
			rc = vlkb::EXIT_WITH_USAGE;
	}
	return rc;
}



//-----------------------------------------------------------
// main
//-----------------------------------------------------------
int main (int argc, char * argv[])
{
	const std::string progname = base_name(argv[0]);

	if( argc < 3 )
	{
		vlkb::usage(progname);
		return vlkb::EXIT_WITH_USAGE;
	}

	const string conf_filename(argv[1]);

	// recognize command from string
	const vlkb::cmd_set cmd(vlkb::to_cmd(argv[2]));

	// sub-command arguments (skips: vlkb <conf-filename>)
	int cmd_argc = argc - 2;
	char ** cmd_argv = &(argv[2]);

	vlkb::conf.read_config(conf_filename);
	/*
		cout << "LOG DIR: " << vlkb::conf.getLogDir() << endl;
		cout << "FITSDIR: " << vlkb::conf.getFitsDir() << endl;
		cout << "DB[" << vlkb::conf.getDbSchema() << "]: " <<vlkb::conf.getDbUri() << endl;
		cout << endl;
	 */
	LOG_open(vlkb::conf.getLogDir(), vlkb::conf.getLogFileName());
	int rc;

	try
	{
		switch(cmd)
		{
			case vlkb::dbinit:         rc = cmd_dbInit(cmd_argc,cmd_argv); break;
			case vlkb::dbcheck:        rc = cmd_dbCheck(); break;
			case vlkb::dblist:         rc = cmd_dbList(); break;
			case vlkb::dbfiles:        rc = cmd_dbFiles(cmd_argc,cmd_argv); break;
			case vlkb::dbaddsurvey:    rc = cmd_dbAdd(cmd_argc,cmd_argv); break;
			case vlkb::dbmodgroups:    rc = cmd_dbModGroups(cmd_argc,cmd_argv); break;
			case vlkb::dbremovesurvey: rc = cmd_dbRemove(cmd_argc,cmd_argv); break;

			default: assert(false);
		}
	}
	catch(const invalid_argument& ex)
	{
		cout << ex.what() << endl;
	}
	catch(const exception& ex)
	{
		cerr <<  ex.what() << endl;
	}

	LOG_close();
	return rc;
}

