using System.Collections.Generic;
using System;
using System.Globalization;
using System.Text;
using Newtonsoft.Json.Linq;
using System.Linq;
using AyaNova.Biz;

namespace AyaNova.DataList
{
    public static class DataListSqlFilterCriteriaBuilder
    {
        public static string DataFilterToSQLCriteria(List<AyaDataListFieldDefinition> objectFieldsList, JArray listViewArray)
        {

            if (listViewArray == null || listViewArray.Count == 0)
            {
                return "";
            }

            //List to compile each columns where clause fragment for later assembly into sql query
            List<string> ColumnWhereClauses = new List<string>();

            for (int i = 0; i < listViewArray.Count; i++)
            {
                StringBuilder sb = new StringBuilder();
                var cm = listViewArray[i];
                //skip it if FILTER is not defined
                if (cm["filter"] == null)
                {
                    continue;
                }



                //Get some info about this column / field
                var fld = cm["fld"].Value<string>();
                var dataType = objectFieldsList.Find(z => z.FieldKey.ToLowerInvariant() == fld).UiFieldDataType;
                AyaDataListFieldDefinition DataListField = objectFieldsList.FirstOrDefault(z => z.FieldKey == fld);

                //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 {fld} specified in template was NOT found in ObjectFields list");
                }
#endif

                //get filter items collection for this field view definition
                var filterItems = (JArray)cm["filter"]["items"];

                var IsOrFilter = false;
                if (cm["filter"]["any"] != null)
                {
                    IsOrFilter = cm["filter"]["any"].Value<bool>();
                }



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

                for (int y = 0; y < filterItems.Count; y++)
                {
                    //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 (IsOrFilter)
                            sb.Append(") OR (");
                        else
                            sb.Append(") AND (");

                    }


                    //gather filter data
                    var filterItem = (JObject)filterItems[y];
                    var opType = filterItem["op"].Value<string>();
                    List<string> tagList = new List<string>();
                    string val = string.Empty;
                    bool IsPossibleIdValue = filterItem["value"].Type == JTokenType.Integer;
                    if (filterItem["value"].Type != JTokenType.Array)
                        val = filterItem["value"].Value<string>();
                    else
                    {
                        tagList = filterItem["value"].ToObject<List<String>>();
                    }
                    //prep for possible ID field filter instead
                    string columnNameToFilter = string.Empty;
                    UiFieldDataType DataTypeToFilter = UiFieldDataType.NoType;
                    //Check if filtering by ID rather than by name
                    //this is indicated by this column being an id type
                    //and by the comparison operator being eq or neq
                    //and the value being a number not a string
                    if (DataListField.HasIdColumn() && IsPossibleIdValue && (opType == DataListFilterComparisonOperator.Equality || opType == DataListFilterComparisonOperator.NotEqual))
                    {
                        columnNameToFilter = DataListField.SqlIdColumnName;
                        DataTypeToFilter = UiFieldDataType.InternalId;
                    }
                    else
                    {
                        DataTypeToFilter = (UiFieldDataType)dataType;
                        columnNameToFilter = DataListField.GetSqlValueColumnName();
                    }

                    //Append this filter's criteria
                    sb.Append(DataFilterToColumnCriteria(columnNameToFilter, DataTypeToFilter, opType, val, tagList));


                    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 Postgres friendly SQL criteria
        /// </summary>
        private static string DataFilterToColumnCriteria(string SqlColumnNameToFilter, UiFieldDataType DataType, string sOperator, string sValue, List<string> sTags)//, bool IsCompound)
        {
            bool TagFilter = sTags.Count < 0;
            StringBuilder sb = new StringBuilder();
            //Column name
            //sb.Append(" ");
            sb.Append(SqlColumnNameToFilter);
            sb.Append(" ");

            //handle null values separately           
            if (!TagFilter && sValue == "*NULL*")
            {
                switch (DataType)
                {
                    case UiFieldDataType.Text:
                        {
                            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 

                //Changed 21-March-2006:
                //But wait!  Both mssql and firebird won't include a null result
                //on the filter field so in some cases we need to include them
                //i.e. if a value is supposed to be not equal to a specific non-null value
                //then null is a valid value to return but won't be normally
                //so....
                //Special addition to handle nulls

                if (!TagFilter)
                {
                    switch (sOperator)
                    {
                        case DataListFilterComparisonOperator.Equality:
                            //no change on equals for nulls
                            break;
                        case DataListFilterComparisonOperator.GreaterThan:
                            //no change on greater than for nulls
                            //(nulls are going to be assumed to be always at the
                            //less than end of the scale)
                            break;
                        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)
                            break;
                        case DataListFilterComparisonOperator.LessThan:
                            sb.Append("Is Null OR ");
                            sb.Append(SqlColumnNameToFilter);
                            sb.Append(" ");
                            break;
                        case DataListFilterComparisonOperator.LessThanOrEqualTo:
                            sb.Append("Is Null OR ");
                            sb.Append(SqlColumnNameToFilter);
                            sb.Append(" ");
                            break;
                        // case "Like":
                        //     //No change on like
                        //     break;
                        case DataListFilterComparisonOperator.NotEqual:
                            //This is the big one:
                            sb.Append("Is Null OR ");
                            sb.Append(SqlColumnNameToFilter);
                            sb.Append(" ");
                            break;
                    }
                }

                #region Build for specific type
                switch (DataType)
                {
                    case UiFieldDataType.Text:
                        //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 alpha bet chunk

                        #region Build TEXT OPS criteria
                        switch (sOperator)
                        {
                            case DataListFilterComparisonOperator.Equality:
                                sb.Append("='");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;

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

                            case DataListFilterComparisonOperator.GreaterThanOrEqualTo:
                                sb.Append(">='");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;

                            case DataListFilterComparisonOperator.LessThan:
                                sb.Append("<'");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;

                            case DataListFilterComparisonOperator.LessThanOrEqualTo:
                                sb.Append("<='");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;

                            case DataListFilterComparisonOperator.NotEqual:
                                sb.Append("<>'");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;


                            /* NOTE: CASE - I decided to NOT do case insensitive for now for datalists like I did for picklists because it's a bit of a different situation
                            I would need to make big changes to the select builder and this criteria builder so it's TTM at the moment because it's all tested and working
                            however also the use is a bit different in that it's much more necessary to be hyper accurate here since this drives reporting and potentially a lot
                            of important business data.  Matching two different clients inadvertantly because of a case issue would be a bit of a kerfuffle potentially so I'd rather err on the side of accuracy
                            and also I'm not certain how the case code will work with other languages so it's a bit more risky here, if a picklist doesn't work I get a support call but if a report is missing data then maybe
                            the user doesn't realize it and has bad reports.

                            code from picklist in case I decide to do it later
                             if (ServerGlobalBizSettings.SearchCaseSensitiveOnly)
                                                            sWhere = $"({valueColumnName} like '%{autoCompleteQuery}%')";
                                                        else
                                                            sWhere = $"(lower({valueColumnName}) like lower('%{autoCompleteQuery}%'))";
                            */

                            //Following 7 operators added 14-June-2006
                            case DataListFilterComparisonOperator.NotContains:
                                sb.Append("Not Like '%");
                                sb.Append(sValue);
                                sb.Append("%'");
                                break;

                            case DataListFilterComparisonOperator.Contains:
                                sb.Append("Like '%");
                                sb.Append(sValue);
                                sb.Append("%'");
                                break;

                            case DataListFilterComparisonOperator.StartsWith:
                                sb.Append("Like '");
                                sb.Append(sValue);
                                sb.Append("%'");
                                break;

                            case DataListFilterComparisonOperator.EndsWith:
                                sb.Append("Like '%");
                                sb.Append(sValue);
                                sb.Append("'");
                                break;

                            // case "DoesNotStartWith":
                            //     sb.Append("Not Like '");
                            //     sb.Append(sValue);
                            //     sb.Append("%'");
                            //     break;

                            // case "DoesNotEndWith":
                            //     sb.Append("Not Like '%");
                            //     sb.Append(sValue);
                            //     sb.Append("'");
                            //     break;

                            // case "NotLike":
                            //     sb.Append("Not Like '");
                            //     sb.Append(sValue);
                            //     sb.Append("%'");
                            //     break;

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

                        }
                        #endregion build text ops criteria
                        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:
                        {
                            //Note: it is assumed all dates come into here from the CLIENT in UTC iso8601 format 
                            //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

                            #region Build criteria for date specified
                            //RAVEN NOTE: it is my intention that dates come in iso8601 UTC format from the client
                            //so a simple parse should be sufficient
                            //https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip
                            System.DateTime dtData = DateTime.Parse(sValue);

                            //THis is because we don't want milliseconds to alter the value in any way, AyaNova resolution for time is seconds not milliseconds to allow for some sloppiness
                            //so no date is exact and we work around that with ranges that rule out milliseconds affecting query
                            //also this needs to work in conjunction with client filters for date ranges that provide a value one second before the range in question as START timestamp
                            //and exactly the end of range in question as END timestamp
                            string sDateValueWithMaxMilliseconds = PostgresDateFormat(MaxMilliseconds(dtData));
                            string sDateValueWithZeroMilliseconds = PostgresDateFormat(ZeroMilliseconds(dtData));

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

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

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

                                case DataListFilterComparisonOperator.NotEqual:
                                    sb.Append("<'");
                                    sb.Append(sDateValueWithZeroMilliseconds);
                                    sb.Append("' OR ");
                                    sb.Append(SqlColumnNameToFilter);
                                    sb.Append(" ");
                                    sb.Append(">'");
                                    sb.Append(sDateValueWithMaxMilliseconds);
                                    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 independant format
                            //RAVEN NOTE: this code looks suspect to me, but I'll leave it in for now
                            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.Tags:
                        {
                            //Build tags filter fragment

                            //NOTE: tag filter OPERATOR is ignored, query matches if all tags are found in a tag collection that could have other tags as well

                            //for initial release a tag filter is inclusive of all tags only
                            //in other words all tags presented must be in record to match (simple AND)
                            //select * from awidget where awidget.tags @> array['blah','blah3'::varchar(255)]

                            //NOTE: After coding this discovered *can* do a LIKE query with tags like this:
                            //(array_to_string(awidget.tags,',') like '%zo%')
                            //implemented that way in picklistquery builder
                            //also ilike is a postgres case insensitive like but works on current locale of server which 
                            //sounds like it might fuck up when using other languages so...
                            StringBuilder sbTags = new StringBuilder();
                            sbTags.Append("@> array[");
                            List<string> normalizedTags = TagBiz.NormalizeTags(sTags);
                            foreach (string s in normalizedTags)
                            {

                                sbTags.Append($"'{s}',");
                            }
                            sb.Append(sbTags.ToString().TrimEnd(','));
                            sb.Append("::varchar(255)]");
                        }
                        break;
                    default:
                        throw new System.ArgumentOutOfRangeException("DATA_TYPE", DataType, "DataListSqlFilterCriteriaBuilder unhandled data type[" + DataType + "]");
                }
                #endregion
            }//end of nonnull path

            return sb.ToString();


        }

        //This is only used by the token date query code above and that code does convert to UTC time to match the DB so this function will not need to 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 BuildBetweenTwoDatesFragment(string sColumn, StringBuilder sb, DateTime dtAfter, DateTime dtBefore)
        {
            sb.Append(">'");
            sb.Append(PostgresDateFormat(dtAfter));
            sb.Append("' AND ");
            sb.Append(sColumn);
            sb.Append(" ");
            sb.Append("<'");
            sb.Append(PostgresDateFormat(dtBefore));
            sb.Append("'");
        }


        private static DateTime ZeroMilliseconds(DateTime d)
        {
            if (d.Millisecond == 0) return d;
            return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, d.Second, DateTimeKind.Utc);
        }
        private static DateTime MaxMilliseconds(DateTime d)
        {
            if (d.Millisecond == 999) return d;
            return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, d.Second, 999, DateTimeKind.Utc);
        }

        /// <summary>
        /// Postgres compatible date format 
        /// https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-DATE-TABLE
        /// </summary>
        /// <returns></returns>
        private static string PostgresDateFormat(DateTime theDate)
        {
            //TODO: Double check that this returns the date as an actual converted to UTC date and not just the local time in UTC format

            //If this was used it should be like this for a UTC date to iso8601
            // ISO8601 with 7 decimal places
            return theDate.ToString("o", CultureInfo.InvariantCulture);

            // if (DBUtil.DB.DBType == DataBaseType.MSSQL)
            //     return "yyyy-MM-ddTHH:mm:ss";
            // else
            //     return "yyyy-MM-dd HH:mm:ss";
        }

    }//eoc
}//ens
