using System.Collections.Generic;
using System;
using System.Globalization;
using System.Text;
using System.Linq;
using Sockeye.Models;
using Sockeye.Biz;
using Sockeye.Util;

namespace Sockeye.DataList
{
    public static class DataListSqlFilterCriteriaBuilder
    {
        public static string DataFilterToSQLCriteria(List<DataListFieldDefinition> objectFieldsList, Models.DataListProcessingBase listOptions)
        {
            if (listOptions.Filter == null || listOptions.Filter.Count == 0)
                return "";
            List<string> ColumnWhereClauses = new List<string>();

            foreach (DataListFilterOption f in listOptions.Filter)
            {
                StringBuilder sb = new StringBuilder();
                DataListFieldDefinition DataListField = objectFieldsList.FirstOrDefault(z => z.FieldKey == f.Column);
                var dataType = DataListField.UiFieldDataType;

                //No filtering on custom fields!
                if (DataListField.IsCustomField)
                {
                    continue;
                }
#if (DEBUG)
                //Developers little helper
                if (DataListField == null)
                {
                    throw new System.ArgumentNullException($"DEV ERROR in DataListSqlFilterCriteriaBuilder.cs: field {f.Column} specified in template was NOT found in ObjectFields list");
                }
#endif

                //Iterate filter items building this WHERE segment
                bool ThisIsTheFirstFilterItemForThisColumn = true;
                var ThereAreMultipleFilterItems = f.Items.Count > 1;

                foreach (DataListColumnFilter filterItem in f.Items)
                {
                    //close or open another parenthetic group

                    //Is this the first filter item of multiple for this field?
                    if (ThisIsTheFirstFilterItemForThisColumn && ThereAreMultipleFilterItems)
                    {
                        //Yup, so put the entire where group for this field in parenthesis
                        sb.Append("(");
                    }

                    //Is this a subsequent filter for this column?
                    if (!ThisIsTheFirstFilterItemForThisColumn)
                    {
                        //Close last filter item and start new one
                        if (f.Any)
                            sb.Append(") OR (");
                        else
                            sb.Append(") AND (");
                    }

                    string columnNameToFilter = string.Empty;
                    UiFieldDataType DataTypeToFilter = UiFieldDataType.NoType;
                    DataTypeToFilter = (UiFieldDataType)dataType;
                    columnNameToFilter = DataListField.GetSqlValueColumnName();
                    //Append this filter's criteria
                    if (DataTypeToFilter != UiFieldDataType.Tags)
                        sb.Append(DataFilterToColumnCriteria(columnNameToFilter, DataTypeToFilter, filterItem.op, filterItem.value, listOptions.ClientTimeStamp));
                    else
                        sb.Append(TagDataFilterToColumnCriteria(columnNameToFilter, filterItem.op, filterItem.value));
                    ThisIsTheFirstFilterItemForThisColumn = false;
                }
                if (ThereAreMultipleFilterItems)
                {
                    //The whole thing was in a group so close this group
                    sb.Append(")");
                }
                ColumnWhereClauses.Add(sb.ToString());
            }
            if (ColumnWhereClauses.Count == 0)
                return string.Empty;
            else
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(" where ");
                for (int i = 0; i < ColumnWhereClauses.Count; i++)
                {
                    sb.Append("(");
                    sb.Append(ColumnWhereClauses[i]);
                    sb.Append(")");
                    if (i < ColumnWhereClauses.Count - 1)
                    {
                        sb.Append(" AND ");
                    }
                }
                return sb.ToString();
            }
        }

        ////////////////////////////////////////////////////////////////////////
        //
        /// <summary>
        /// Translate DataFilter to PostgreSQL friendly SQL criteria
        /// </summary>
        public static string DataFilterToColumnCriteria(string SqlColumnNameToFilter,
            UiFieldDataType DataType,
            string sOperator,
            string sValue,
            DateTimeOffset clientTimeStamp)
        {
            StringBuilder sb = new StringBuilder();

            //handle null values separately           
            if (sValue == "*NULL*")
            {
                //Column name
                sb.Append(SqlColumnNameToFilter); //case doesn't matter for null searches
                sb.Append(" ");

                switch (DataType)
                {
                    //ALL TEXT TYPES
                    case UiFieldDataType.Text:
                    case UiFieldDataType.PhoneNumber:
                    case UiFieldDataType.EmailAddress:
                    case UiFieldDataType.HTTP:
                        {
                            if (sOperator == DataListFilterComparisonOperator.Equality)
                            {

                                sb.Append("Is Null");
                                sb.Append(" OR ");
                                sb.Append(SqlColumnNameToFilter);
                                sb.Append(" = ''");
                            }
                            else
                                sb.Append(" <> ''");
                        }
                        break;
                    default:
                        {
                            if (sOperator == DataListFilterComparisonOperator.Equality)
                                sb.Append("Is Null");
                            else
                                sb.Append("Is Not Null");
                        }
                        break;
                }
            }
            else
            {
                //non null value 

                bool ForceToLower = false;
                //Force to LOWER case if it's a text data type and filter case sensitive is false
                if (!ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                {
                    switch (DataType)
                    {
                        //ALL TEXT TYPES
                        case UiFieldDataType.Text:
                        case UiFieldDataType.PhoneNumber:
                        case UiFieldDataType.EmailAddress:
                        case UiFieldDataType.HTTP:
                            ForceToLower = true;
                            break;

                    }
                }

                //Handle nulls and also insert the column name into the query in case sensitive fashion
                switch (sOperator)
                {
                    //case DataListFilterComparisonOperator.Equality:   //no specific addition on equals for nulls, let default handle it                      
                    //case DataListFilterComparisonOperator.GreaterThan:
                    //no specific addition on greater than for nulls
                    //(nulls are going to be assumed to be always at the
                    //less than end of the scale) so let default handle it                      
                    //case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                    //no change on greater than for nulls
                    //(nulls are going to be assumed to be always at the
                    //less than end of the scale) let default case handle it

                    case DataListFilterComparisonOperator.LessThan:
                        sb.Append($"{SqlColumnNameToFilter} ");
                        sb.Append("Is Null OR ");
                        if (ForceToLower)
                            sb.Append($"lower({SqlColumnNameToFilter})");
                        else
                            sb.Append(SqlColumnNameToFilter);
                        sb.Append(" ");
                        break;
                    case DataListFilterComparisonOperator.LessThanOrEqualTo:
                        sb.Append($"{SqlColumnNameToFilter} ");
                        sb.Append("Is Null OR ");
                        if (ForceToLower)
                            sb.Append($"lower({SqlColumnNameToFilter})");
                        else
                            sb.Append(SqlColumnNameToFilter);
                        sb.Append(" ");
                        break;
                    case DataListFilterComparisonOperator.NotEqual:
                        sb.Append($"{SqlColumnNameToFilter} ");
                        //This is the big one:
                        sb.Append("Is Null OR ");
                        if (ForceToLower)
                            sb.Append($"lower({SqlColumnNameToFilter})");
                        else
                            sb.Append(SqlColumnNameToFilter);
                        sb.Append(" ");
                        break;
                    default:
                        if (ForceToLower)
                            sb.Append($"lower({SqlColumnNameToFilter}) ");
                        else
                            sb.Append($"{SqlColumnNameToFilter} ");
                        break;
                }


                #region Build for specific type
                switch (DataType)
                {
                    //ALL TEXT TYPES
                    case UiFieldDataType.Text:
                    case UiFieldDataType.PhoneNumber:
                    case UiFieldDataType.EmailAddress:
                    case UiFieldDataType.HTTP:
                        //escape any pre-existing apostrophes
                        //i.e. "O'Flaherty's pub"
                        sValue = sValue.Replace("'", "''");

                        //case 1951 - unescape ampersands
                        sValue = sValue.Replace("&amp;", "&");

                        //RAVEN NOTE: Decided not to implement TEXT token criteria for now (TTM, probably not useful)
                        //meaning the A-H etc group ranges by alphabet chunk

                        #region Build TEXT OPS criteria
                        switch (sOperator)
                        {
                            case DataListFilterComparisonOperator.Equality:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"= '{sValue}'");
                                else
                                    sb.Append($"= lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.GreaterThan:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"> '{sValue}'");
                                else
                                    sb.Append($"> lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($">= '{sValue}'");
                                else
                                    sb.Append($">= lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.LessThan:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"< '{sValue}'");
                                else
                                    sb.Append($"< lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.LessThanOrEqualTo:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"<= '{sValue}'");
                                else
                                    sb.Append($"<= lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.NotEqual:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"<> '{sValue}'");
                                else
                                    sb.Append($"<> lower('{sValue}')");
                                break;

                            case DataListFilterComparisonOperator.NotContains:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"NOT LIKE '%{sValue}%'");
                                else
                                    sb.Append($"NOT LIKE lower('%{sValue}%')");
                                break;

                            case DataListFilterComparisonOperator.Contains:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"LIKE '%{sValue}%'");
                                else
                                    sb.Append($"LIKE lower('%{sValue}%')");
                                break;

                            case DataListFilterComparisonOperator.StartsWith:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"LIKE '{sValue}%'");
                                else
                                    sb.Append($"LIKE lower('{sValue}%')");
                                break;

                            case DataListFilterComparisonOperator.EndsWith:
                                if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
                                    sb.Append($"LIKE '%{sValue}'");
                                else
                                    sb.Append($"LIKE lower('%{sValue}')");
                                break;

                            default:
                                throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN STRING");

                        }
                        #endregion build text ops criteria
                        break;


                    case UiFieldDataType.Roles:
                        {
                            /*
                            So Roles can be queried as contains or NOT contains with a bitwise &
                            also a Equals or NOt equals with a direct equality comparison

                            CONTAINS:
                            User selects roles, query checks if a user has all the roles the user selected with this query:
                            single OpsAdminRestricted (8192):
                            select name, roles from auser where (roles & 8192 = 8192) order by name

                            User has OpsAdminRestricted (8192) and SubContractorRestricted (512) which client will return as 8704:
                            select name, roles from auser where (roles & 8704 = 8704) order by name
                            brings up users with both opsadminrestricted and subconctractorrestricted and possibly others doesn't matter


                            EQUALITY:
                            just a simple = or <> i.e. where roles=4096 or roles!=4096
                            */
                            switch (sOperator)
                            {
                                case DataListFilterComparisonOperator.Equality:
                                    sb.Append($"= {sValue} ");
                                    break;

                                case DataListFilterComparisonOperator.NotEqual:
                                    sb.Append($"<> {sValue}");
                                    break;
                                case DataListFilterComparisonOperator.Contains:
                                    sb.Append($"& {sValue}={sValue} ");
                                    break;
                                case DataListFilterComparisonOperator.NotContains:
                                    sb.Append($"& {sValue}!={sValue} ");
                                    break;
                                default:
                                    throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] in ROLES");

                            }
                        }
                        break;

                    case UiFieldDataType.Bool:
                        {
                            switch (sOperator)
                            {
                                case DataListFilterComparisonOperator.Equality:
                                    sb.Append("= ");
                                    if (sValue.ToLowerInvariant() == "true")
                                        sb.Append("true");
                                    else
                                        sb.Append("false");
                                    break;

                                case DataListFilterComparisonOperator.NotEqual:
                                    sb.Append("<> ");
                                    if (sValue.ToLowerInvariant() == "true")
                                        sb.Append("true");
                                    else
                                        sb.Append("false");

                                    break;
                                default:
                                    throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] in BOOL");

                            }
                        }
                        break;

                    //Note there are three types here for display purposes but all are stored in the db as a timestamp the same with date and time components
                    case UiFieldDataType.Date:
                    case UiFieldDataType.DateTime:
                    case UiFieldDataType.Time:
                        {

                            if (sValue.StartsWith("*") && sValue.EndsWith("*"))
                            {
                                //Note: due to date token relativity, all below calcs done with Client provided current date/time and converted to UTC only at the end for sql
                                DateTime ClientToday = clientTimeStamp.Date;
                                DateTime ClientNow = clientTimeStamp.DateTime;

                                #region Build criteria for date RANGE TOKEN specified

                                //Used as the basis point
                                System.DateTime dtAfter;
                                System.DateTime dtBefore;
                                switch (sValue)
                                {
                                    //Case 402
                                    case "*yesterday*":
                                        //Between Day before yesterday at midnight and yesterday at midnight
                                        dtAfter = ClientToday.AddDays(-1);
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        dtBefore = ClientToday;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*today*":
                                        //Between yesterday at midnight and tommorow at midnight
                                        dtAfter = ClientToday.AddSeconds(-1);
                                        dtBefore = ClientToday.AddDays(1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*tomorrow*":
                                        //Between Tonight at midnight and day after tommorow at midnight
                                        dtAfter = ClientToday.AddDays(1).AddSeconds(-1);
                                        dtBefore = ClientToday.AddDays(2);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*lastweek*":
                                        //Between two Sundays ago at midnight and last sunday at midnight
                                        dtAfter = ClientToday;
                                        //go back a week
                                        dtAfter = dtAfter.AddDays(-7);
                                        //go backwards to Sunday
                                        while (dtAfter.DayOfWeek != DayOfWeek.Sunday)
                                            dtAfter = dtAfter.AddDays(-1);
                                        //go to very start of eighth dayahead
                                        dtBefore = dtAfter.AddDays(8);
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*thisweek*":
                                        //Between Sunday at midnight and Next sunday at midnight
                                        dtAfter = ClientToday;
                                        //go backwards to monday
                                        while (dtAfter.DayOfWeek != DayOfWeek.Monday)
                                            dtAfter = dtAfter.AddDays(-1);

                                        //Now go back to sunday last second
                                        dtAfter = dtAfter.AddSeconds(-1);

                                        dtBefore = ClientToday;
                                        //go forwards to monday
                                        if (ClientToday.DayOfWeek == DayOfWeek.Monday)
                                        {
                                            //Monday today? then go to next monday
                                            dtBefore = dtBefore.AddDays(7);
                                        }
                                        else
                                        {
                                            while (dtBefore.DayOfWeek != DayOfWeek.Monday)
                                                dtBefore = dtBefore.AddDays(1);
                                        }

                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*nextweek*":
                                        //Between Next Sunday at midnight and Next Next sunday at midnight
                                        dtAfter = ClientToday;

                                        //If today is monday skip over it first
                                        if (dtAfter.DayOfWeek == DayOfWeek.Monday)
                                            dtAfter = dtAfter.AddDays(1);

                                        //go forwards to next monday
                                        while (dtAfter.DayOfWeek != DayOfWeek.Monday)
                                            dtAfter = dtAfter.AddDays(1);
                                        //Now go back to sunday last second
                                        dtAfter = dtAfter.AddDays(-1);
                                        //go seven days ahead
                                        dtBefore = dtAfter.AddDays(7);
                                        dtAfter = dtAfter.AddSeconds(-1);

                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*lastmonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
                                        //subtract a Month
                                        dtAfter = dtAfter.AddMonths(-1);

                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);

                                        //case 1155
                                        dtAfter = dtAfter.AddSeconds(-1);

                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*thismonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);

                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);

                                        //case 1155
                                        dtAfter = dtAfter.AddSeconds(-1);

                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*nextmonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
                                        //Add a Month
                                        dtAfter = dtAfter.AddMonths(1);

                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);

                                        //case 1155
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*14daywindow*":
                                        //start with today zero hour
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, ClientToday.Day);
                                        dtAfter = dtAfter.AddDays(-7);

                                        //Add 15 days to get end date (zero hour so not really 15 full days)
                                        dtBefore = dtAfter.AddDays(15);

                                        //case 1155
                                        dtAfter = dtAfter.AddSeconds(-1);

                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*past*":
                                        //Forever up to Now 
                                        dtAfter = new DateTime(1753, 1, 2);//this was for sql server but even then was probably outdated good enough though for our purposes
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*future*":
                                        //From Now to forever (999 years from now)                                        
                                        dtAfter = ClientNow;
                                        dtBefore = dtAfter.AddYears(999);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*lastyear*":
                                        //From zero hour january 1 a year ago
                                        dtAfter = new DateTime(ClientNow.AddYears(-1).Year, 1, 1, 0, 0, 00).AddSeconds(-1);
                                        //To zero hour January 1 this year
                                        dtBefore = new DateTime(ClientNow.Year, 1, 1, 0, 0, 00);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*thisyear*":
                                        //From zero hour january 1 this year
                                        dtAfter = new DateTime(ClientNow.Year, 1, 1).AddSeconds(-1);
                                        //To zero hour Jan 1 next year
                                        dtBefore = new DateTime(ClientNow.AddYears(1).Year, 1, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*last3months*":
                                        //From Now minus 3 months
                                        dtAfter = ClientNow.AddMonths(-3).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*last6months*":
                                        //From Now minus 6 months
                                        dtAfter = ClientNow.AddMonths(-6).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*pastyear*": //within the prior 365 days before today
                                        //From Now minus 365 days
                                        dtAfter = ClientNow.AddDays(-365).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*past90days*":
                                        //From Now minus 90 days
                                        dtAfter = ClientNow.AddDays(-90).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*past30days*":
                                        //From Now minus 30 days
                                        dtAfter = ClientNow.AddDays(-30).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*past7days*":
                                        //From Now minus 7 days
                                        dtAfter = ClientNow.AddDays(-7).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*past24hours*":
                                        //From Now minus 24 hours
                                        dtAfter = ClientNow.AddHours(-24).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*past6hours*":
                                        //From Now minus 6 hours
                                        dtAfter = ClientNow.AddHours(-6).AddSeconds(-1);
                                        //To Now
                                        dtBefore = ClientNow;
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*january*":
                                        //From zero hour january 1 this year
                                        dtAfter = new DateTime(ClientNow.Year, 1, 1).AddSeconds(-1);
                                        //To zero hour feb 1 this year
                                        dtBefore = new DateTime(ClientNow.Year, 2, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*february*":
                                        dtAfter = new DateTime(ClientNow.Year, 2, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 3, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*march*":
                                        dtAfter = new DateTime(ClientNow.Year, 3, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 4, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*april*":
                                        dtAfter = new DateTime(ClientNow.Year, 4, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 5, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*may*":
                                        dtAfter = new DateTime(ClientNow.Year, 5, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 6, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*june*":
                                        dtAfter = new DateTime(ClientNow.Year, 6, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 7, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*july*":
                                        dtAfter = new DateTime(ClientNow.Year, 7, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 8, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*august*":
                                        dtAfter = new DateTime(ClientNow.Year, 8, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 9, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*september*":
                                        dtAfter = new DateTime(ClientNow.Year, 9, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 10, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*october*":
                                        dtAfter = new DateTime(ClientNow.Year, 10, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 11, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*november*":
                                        dtAfter = new DateTime(ClientNow.Year, 11, 1).AddSeconds(-1);
                                        dtBefore = new DateTime(ClientNow.Year, 12, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*december*":
                                        //From zero hour dec 1 this year
                                        dtAfter = new DateTime(ClientNow.Year, 12, 1).AddSeconds(-1);
                                        //To zero hour Jan 1 next year
                                        dtBefore = new DateTime(ClientNow.AddYears(1).Year, 1, 1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;
                                    case "*lastyearlastmonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
                                        //subtract a year and a Month
                                        dtAfter = dtAfter.AddYears(-1).AddMonths(-1);
                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*lastyearthismonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
                                        //subtract a year
                                        dtAfter = dtAfter.AddYears(-1);
                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    case "*lastyearnextmonth*":
                                        //start with the first day of this month
                                        dtAfter = new DateTime(ClientToday.Year, ClientToday.Month, 1);
                                        //subtract a year
                                        dtAfter = dtAfter.AddYears(-1);
                                        //Add a Month
                                        dtAfter = dtAfter.AddMonths(1);
                                        //Add one month to dtAfter to get end date
                                        dtBefore = dtAfter.AddMonths(1);
                                        dtAfter = dtAfter.AddSeconds(-1);
                                        BuildSQLBetweenTwoTokenDerivedDates(SqlColumnNameToFilter, sb, dtAfter, dtBefore);
                                        break;

                                    default:
                                        throw new System.ArgumentOutOfRangeException("TOKEN", sOperator, "DataListSqlFilterCriteriaBuilder invalid filter TOKEN type [" + sValue + "] IN DATE_TIME");

                                        //-----
                                }

                                #endregion
                            }
                            else
                            {


                                #region Build criteria for date specified
                                //dates come in iso8601 UTC format from the client
                                //suitable for the database to handle as all database dates are in UTC
                                //Local display and parsing will be considered a CLIENT issue at all times

                                //comes in UTC, parse converts it to local but touniversal puts it back in utc, weird but only thing that consistently work
                                System.DateTime dtData = DateTime.Parse(sValue).ToUniversalTime();

                                //Filter time resolution is in minutes only, you can't select seconds in a filter
                                //so the filter code below must construct a filter time 
                                //that falls within the 0th second, 0th millisecond and 59th second and 999th millisecond of the specified time
                                //to ensure it matches the users selections
                                string sDateValueWithZeroSeconds = PostgresDateFormat(ZeroSeconds(dtData));
                                string sDateValueWithMaxSeconds = PostgresDateFormat(MaxSeconds(dtData));

                                switch (sOperator)
                                {
                                    case DataListFilterComparisonOperator.Equality:
                                        sb.Append(">='");
                                        sb.Append(sDateValueWithZeroSeconds);
                                        sb.Append("' AND ");
                                        sb.Append(SqlColumnNameToFilter);
                                        sb.Append(" ");
                                        sb.Append("<='");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");
                                        break;

                                    case DataListFilterComparisonOperator.GreaterThan:
                                        sb.Append(">'");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");
                                        break;

                                    case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                                        sb.Append(">='");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");
                                        break;
                                    case DataListFilterComparisonOperator.LessThan:
                                        sb.Append("<'");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");
                                        break;
                                    case DataListFilterComparisonOperator.LessThanOrEqualTo:
                                        sb.Append("<='");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");
                                        break;

                                    case DataListFilterComparisonOperator.NotEqual:
                                        sb.Append("<'");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("' OR ");
                                        sb.Append(SqlColumnNameToFilter);
                                        sb.Append(" ");
                                        sb.Append(">'");
                                        sb.Append(sDateValueWithMaxSeconds);
                                        sb.Append("'");

                                        break;
                                    default:
                                        throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN DATE_TIME");


                                }
                                #endregion
                            }
                        }
                        break;
                    case UiFieldDataType.Enum://enums are just ints to the db, but it's a special type so the client can recognize it
                    case UiFieldDataType.MemorySize://memory / file size, just a long but this type is for display formatting only
                    case UiFieldDataType.Decimal:
                    case UiFieldDataType.Currency:
                    case UiFieldDataType.InternalId:
                    case UiFieldDataType.Integer:    //whole numbers, not only integer                
                        {
                            //case 1795 - it's numeric, convert to translation independent format
                            //RAVEN NOTE: no numbers are coming from the client in any culture format other than xx,xxx.00 but this is just insurance for api users
                            NumberFormatInfo nfi = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
                            switch (DataType)
                            {
                                case UiFieldDataType.Decimal:
                                case UiFieldDataType.Currency:
                                    {
                                        if (nfi.CurrencyDecimalSeparator != ".")
                                        {
                                            sValue = sValue.Replace(nfi.CurrencyGroupSeparator, "");
                                            sValue = sValue.Replace(nfi.CurrencyDecimalSeparator, ".");
                                        }
                                    }
                                    break;
                                case UiFieldDataType.Integer:
                                    {
                                        if (nfi.NumberDecimalSeparator != ".")
                                        {
                                            sValue = sValue.Replace(nfi.NumberGroupSeparator, "");
                                            sValue = sValue.Replace(nfi.NumberDecimalSeparator, ".");
                                        }
                                    }
                                    break;
                                case UiFieldDataType.InternalId:
                                    {
                                        //do nothing, it's a simple number
                                    }
                                    break;
                            }

                            switch (sOperator)
                            {
                                case DataListFilterComparisonOperator.Equality:
                                    sb.Append("=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.GreaterThan:
                                    sb.Append(">");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                                    sb.Append(">=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.LessThan:
                                    sb.Append("<");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.LessThanOrEqualTo:
                                    sb.Append("<=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.NotEqual:
                                    sb.Append("<>");
                                    sb.Append(sValue);
                                    break;
                                default:
                                    throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN NUMBER");

                            }
                            break;
                        }

                    case UiFieldDataType.TimeSpan:    //TIMESPAN / DURATION     
                        {
                            sValue = TimeSpanToPostgresInterval(sValue);
                            /*
                             { name: vm.$sock.t("GridRowFilterDropDownGreaterThan"), id: ">" },
                                  {
                                    name: vm.$sock.t("GridRowFilterDropDownGreaterThanOrEqualTo"),
                                    id: ">="
                                  },
                                  { name: vm.$sock.t("GridRowFilterDropDownLessThan"), id: "<" },
                                  { name: vm.$sock.t("GridRowFilterDropDownLessThanOrEqualTo"), id: "<=" },
                                  { name: vm.$sock.t("GridRowFilterDropDownNotEquals"), id: "!=" }
                            */
                            switch (sOperator)
                            {
                                case DataListFilterComparisonOperator.Equality:
                                    sb.Append("=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.GreaterThan:
                                    sb.Append(">");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                                    sb.Append(">=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.LessThan:
                                    sb.Append("<");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.LessThanOrEqualTo:
                                    sb.Append("<=");
                                    sb.Append(sValue);
                                    break;
                                case DataListFilterComparisonOperator.NotEqual:
                                    sb.Append("<>");
                                    sb.Append(sValue);
                                    break;
                                default:
                                    throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN TIMESPAN / DURATION");

                            }
                            break;
                        }
                    default:
                        throw new System.ArgumentOutOfRangeException("DATA_TYPE", DataType, "DataListSqlFilterCriteriaBuilder unhandled data type[" + DataType + "]");
                }
                #endregion
            }//end of nonnull path

            return sb.ToString();


        }



        ////////////////////////////////////////////////////////////////////////
        //
        /// <summary>
        /// Translate TAG DataFilter to PostgreSQL friendly SQL criteria
        /// </summary>
        public static string TagDataFilterToColumnCriteria(string SqlColumnNameToFilter, string sOperator, string sValue)
        {

            /*
            https://www.postgresql.org/docs/current/functions-array.html

            Filter ui builder allows multiple tag selection just like any other tag selection and sends the filter to the server like this:
            filter	"[{\"column\":\"customertags\",\"any\":false,\"items\":[{\"op\":\"=\",\"value\":\"completed.reminder,burnaby.dispatchzone,cabling-fibre.user-skill\"}]}]"
            Comma separated list.
            Current equalto filter doesn't work with this type of query as it directly compares it as if it was one string instead of does it contain all those terms

            ### FILTERS AVAILABLE ###

            Equality: Exactly equal (aside from order) compare entire search term array to tag array in db - all terms in search exactly present and no others in db record
                contains and also length is same as search terms length, in this example 1 is the number of search terms as the search is for {'green'}
                WHERE ARRAY['green'::varchar(255)] <@ tags and  array_length(tags,1) = 1 

            Not equal: Not EXACTLY Equal (aside from order) All terms in search term array NOT present in All terms in db record. IOW if db record has all search terms but also one more tag then it's NOT equal, Order doesn't matter
                Specifically exclude rows that exactly match all the search terms
                WHERE NOT (ARRAY['green'::varchar(255)] <@ tags and  array_length(tags,1) = 1)

            NO value: Empty tag array in db record
                db record tags has NO value
                where tags = '{}'

            Has value: Non empty tag array in db record
                db record tags has value
                where tags <> '{}'

            Contains: All terms in search query present in db record, db record may have other tags but that's ok as long as it has the search term ones (order insensitive)
                db record Tags contains search tags (and maybe also other tags)
                WHERE ARRAY['red','green'::varchar(255)] <@ tags 

            Not contains: All terms in search query *NOT* present in db record, don't care what else is in db record as long as it's not the search terms (order insensitive)
                db record Tags DOES NOT contain search tags 
                WHERE NOT ARRAY['red','green'::varchar(255)] <@ tags

            This method and choices ensures users don't need to make a separate row for each one they can just treat the entire tag collection like they treat a single word match in a text field
            */

            //HAS VALUE / NO VALUE
            if (sValue == "*NULL*")
            {
                if (sOperator == DataListFilterComparisonOperator.Equality)
                    return $"{SqlColumnNameToFilter} = '{{}}'";
                else
                    return $"{SqlColumnNameToFilter} <> '{{}}'";
            }
            else
            {
                string PostgresTagArrayFragment = string.Empty;
                //Filter ui builder allows multiple tag selection just like any other tag selection and sends the filter to the server like this:
                //filter	"[{\"column\":\"customertags\",\"any\":false,\"items\":[{\"op\":\"=\",\"value\":\"completed.reminder,burnaby.dispatchzone,cabling-fibre.user-skill\"}]}]"
                //needs to be "ARRAY['red','green'::varchar(255)]"
                StringBuilder sbTemp = new StringBuilder();
                sbTemp = sbTemp.Append("ARRAY[");
                List<string> normalizedTags = TagBiz.NormalizeTags(sValue.Split(',').ToList<string>());
                if (normalizedTags.Count == 0)
                    throw new System.ArgumentNullException("DataListSqlFilterCriteriaBuilder::TagDataFilterToColumnCriteria - NO tags were provided for filtering");

                foreach (string s in normalizedTags)
                {
                    //escape any pre-existing apostrophes
                    //i.e. "O'Flaherty's pub"
                    var cleaned = s.Replace("'", "''");
                    sbTemp.Append($"'{cleaned}',");
                }
                PostgresTagArrayFragment = sbTemp.ToString().TrimEnd(',');
                PostgresTagArrayFragment += "::VARCHAR(255)]";

                switch (sOperator)
                {
                    case DataListFilterComparisonOperator.Equality:
                        //ARRAY['green'::varchar(255)] <@ tags and  array_length(tags,1) = 1                             
                        return $"{PostgresTagArrayFragment} <@ {SqlColumnNameToFilter} AND ARRAY_LENGTH({SqlColumnNameToFilter},1) = {normalizedTags.Count}";
                    case DataListFilterComparisonOperator.NotEqual:
                        //NOT (ARRAY['green'::varchar(255)] <@ tags and  array_length(tags,1) = 1)                            
                        return $"NOT ({PostgresTagArrayFragment} <@ {SqlColumnNameToFilter} AND ARRAY_LENGTH({SqlColumnNameToFilter},1) = {normalizedTags.Count})";
                    case DataListFilterComparisonOperator.NotContains:
                        //NOT ARRAY['red','green'::varchar(255)] <@ tags                             
                        return $"NOT {PostgresTagArrayFragment} <@ {SqlColumnNameToFilter}";
                    case DataListFilterComparisonOperator.Contains:
                        //ARRAY['red','green'::varchar(255)] <@ tags                             
                        return $"{PostgresTagArrayFragment} <@ {SqlColumnNameToFilter}";
                    default:
                        throw new System.ArgumentOutOfRangeException("OPERATOR_TYPE", sOperator, "DataListSqlFilterCriteriaBuilder unhandled operator type [" + sOperator + "] IN TAGS");
                }
            }
        }




        //This is only used by the token date query code above and that code does NOT convert to UTC time to match the DB so this function will handle that
        //The other non tokenized date criteria builders are all working with dates that come from the client in UTC already and they don't use this method 
        //so nothing required there
        private static void BuildSQLBetweenTwoTokenDerivedDates(string sColumn, StringBuilder sb, DateTime dtAfter, DateTime dtBefore)
        {

            sb.Append(">'");
            sb.Append(PostgresDateFormat(MaxSeconds(dtAfter.ToUniversalTime())));
            sb.Append("' AND ");
            sb.Append(sColumn);
            sb.Append(" ");
            sb.Append("<'");
            sb.Append(PostgresDateFormat(ZeroSeconds(dtBefore.ToUniversalTime())));
            sb.Append("'");
        }


        private static DateTime ZeroSeconds(DateTime d)
        {
            return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0, DateTimeKind.Utc);
        }



        private static DateTime MaxSeconds(DateTime d)
        {
            return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 59, 999, DateTimeKind.Utc);
        }


        /// <summary>
        /// PostgreSQL compatible date format 
        /// https://www.postgresql.org/docs/current/datsockType-datetime.html#DATATYPE-DATETIME-DATE-TABLE
        /// </summary>
        /// <returns></returns>
        private static string PostgresDateFormat(DateTime theDate)
        {
            // ISO8601 with 7 decimal places
            return theDate.ToString("o", CultureInfo.InvariantCulture);
        }



        ///////////////////////////////////////////
        // 
        //
        private static string TimeSpanToPostgresInterval(string value)
        {
            //ISO 8601 duration format "brief" or short
            //     "5.0:0:0" <-what it looks like as a timespan string
            //   P6Y5M4DT3H2M1S <- What a postgres compatible iso 8601 format looks like
            // interval 'P6Y5M4DT3H2M1S' <- actual query fragment 

            int theDays = 0;
            int theHours = 0;
            int theMinutes = 0;
            int theSeconds = 0;

            if (string.IsNullOrWhiteSpace(value))
            {
                return "INTERVAL 'P0DT0H0M0S'";
            }
            else
            {
                var work = value.Split(":");
                //has days?
                if (work[0].Contains("."))
                {
                    var dh = work[0].Split(".");
                    theDays = int.Parse(dh[0]);
                    theHours = int.Parse(dh[1]);
                }
                else
                {
                    theHours = int.Parse(work[0]);
                }
                theMinutes = int.Parse(work[1]);
                //has milliseconds? (ignore them)
                if (work[2].Contains("."))
                {
                    var dh = work[2].Split(".");
                    theSeconds = int.Parse(dh[0]);
                }
                else
                {
                    theSeconds = int.Parse(work[2]);
                }

                //format return string
                return $"INTERVAL 'P{theDays}DT{theHours}H{theMinutes}M{theSeconds}S'";

            }
        }


        ////////////////////////////////////////////////////////////////////////
        /// <summary>
        /// Translate tag filter to PostgreSQL friendly SQL criteria
        /// </summary>
        ///
        public static string TagFilterToSqlCriteriaHelper(string SqlColumnNameToFilter, List<string> sValue, bool bAny)
        {
            if(sValue.Count==0) return string.Empty;
            //if it's an OR (any) query then need to build individual terms, if it's not then it's just an all or AND query and can pass through as is
            if (sValue.Count > 0 && bAny == true)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(" AND ");
                sb.Append("(");
                foreach (string s in sValue)
                {
                    sb.Append($"({TagDataFilterToColumnCriteria(SqlColumnNameToFilter, DataListFilterComparisonOperator.Contains, s)}) OR ");
                }
                var ret = sb.ToString();
                return ret.Substring(0, ret.LastIndexOf(" OR ")) + ")";
            }
            else
            {
                //it's an AND (not any) query so it's just all inclusive but still contains
                return " AND " + TagDataFilterToColumnCriteria(SqlColumnNameToFilter, DataListFilterComparisonOperator.Contains, string.Join(",", sValue));
            }
        }



    }//eoc
}//ens