using Newtonsoft.Json.Linq;
using OI.Metrology.Shared.DataModels;
using OI.Metrology.Shared.Models.Stateless;
using OI.Metrology.Shared.Services;

#pragma warning disable CS8600, CS8602, CS8603, CS8604, CS8625

namespace OI.Metrology.Viewer.Services;

public class InboundDataService : IInboundDataService
{
    private readonly IMetrologyRepository _MetrologyRepository;

    public InboundDataService(IMetrologyRepository metrologyRepository) => _MetrologyRepository = metrologyRepository;

    public long DoSQLInsert(JToken jsonbody, ToolType toolType, List<ToolTypeMetadata> metaData)
    {
        JArray detailsArray = null;
        string uniqueId = "";

        foreach (JToken jt in jsonbody.Children())
        {
            if (jt is JProperty jp)
            {
                if (string.Equals(jp.Name, "Details", StringComparison.OrdinalIgnoreCase))
                {
                    if (jp.First is JArray array)
                        detailsArray = array;
                }
                else if (string.Equals(jp.Name, "UniqueId", StringComparison.OrdinalIgnoreCase))
                {
                    uniqueId = Convert.ToString(((JValue)jp.Value).Value);
                }
            }
        }

        long headerId = 0;

        using (System.Transactions.TransactionScope transScope = _MetrologyRepository.StartTransaction())
        {
            try
            {
                _MetrologyRepository.PurgeExistingData(toolType.ID, uniqueId);
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to purge existing data: " + ex.Message, ex);
            }

            try
            {
                headerId = _MetrologyRepository.InsertToolDataJSON(jsonbody, -1, metaData, toolType.HeaderTableName);
            }
            catch (Exception ex)
            {
                throw new Exception("Insert failed for header: " + ex.Message, ex);
            }

            int detailrow = 1;
            try
            {
                if (detailsArray is not null)
                {
                    foreach (JToken detail in detailsArray)
                    {
                        _ = _MetrologyRepository.InsertToolDataJSON(detail, headerId, metaData, toolType.DataTableName);
                        detailrow += 1;
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Insert failed for detail row " + detailrow.ToString() + ": " + ex.Message, ex);
            }

            transScope.Complete();
        }

        return headerId;
    }

    // this method is for validating the json contents of the inbound request, it will make sure all required fields are included
    // errors are generated for missing fields, and warnings are generated for additional fields not in the metadata
    // this is recursive, detailIndex = 0 is for the header, then it calls itself for each of the details rows (in any)
    public void ValidateJSONFields(JToken jsonbody, int detailIndex, List<ToolTypeMetadata> metaData, List<string> errors, List<string> warnings)
    {
        bool isHeader = detailIndex == 0;
        string rowDesc = isHeader ? "header" : "detail index " + detailIndex.ToString(); // human readable description for error messages

        // filter the metadata list by header/detail
        List<ToolTypeMetadata> fields = metaData.Where(md => md.Header == isHeader).ToList();

        // get list of ApiFields from the metadata, exclude blank ApiName
        List<string> apiFields = fields.Where(f => string.IsNullOrWhiteSpace(f.ApiName) == false).Select(f => f.ApiName.Trim().ToUpper()).ToList();

        // get list of ApiFields from the metadata with blank ColumnName - we ignore these fields in the jsonbody
        List<string> ignoreFields = fields.Where(f => (string.IsNullOrWhiteSpace(f.ApiName) == false) && string.IsNullOrWhiteSpace(f.ColumnName)).Select(f => f.ApiName.Trim().ToUpper()).ToList();

        // keep a list of valid fields found in the jsonbody so we can check for duplicates
        List<string> validFields = new();

        // get list of container fields in the ApiFields, ex: Points\Thickness will add Points to the list
        List<string> containerFields = apiFields.Where(f => f.Contains('\\')).Select(f => f.Split('\\')[0].Trim().ToUpper()).Distinct().ToList();

        // pointer to the Details array from the jsonbody, this is hard-coded as the subfield for the common Header/Detail structure
        JArray detailsArray = null;

        // process fields in the json body
        foreach (JToken jt in jsonbody.Children())
        {
            if (jt is JProperty jp)
            {
                string jpName = jp.Name.Trim().ToUpper();

                if (apiFields.Contains(jpName))
                {
                    // Normal field detected, remove it from the list so we know which fields are missing
                    _ = apiFields.Remove(jpName);

                    // Check for duplicates
                    if (validFields.Contains(jpName))
                        errors.Add("Duplicated field on " + rowDesc + ": " + jp.Name);
                    else
                        validFields.Add(jpName);
                }
                else if (string.Equals(jp.Name, "Details", StringComparison.OrdinalIgnoreCase))
                {
                    // Details container field found

                    if (!isHeader)
                        errors.Add("Details field not expected on " + rowDesc);

                    if (jp.First is JArray array)
                        detailsArray = array;
                    else if ((jp.First is JValue value) && (value.Value is null))
                        detailsArray = null;
                    else
                        errors.Add("Invalid details field");
                }
                else if (ignoreFields.Contains(jpName))
                {
                    // ignore these fields 
                }
                else if (containerFields.Contains(jpName))
                {
                    // ignore fields that are container fields
                }
                else
                {
                    // output warnings if extra fields are found
                    warnings.Add("Extra field on " + rowDesc + ": " + jp.Name);
                }
            }
        }

        // process container fields, ex: Points
        ValidateJSONContainerFields(jsonbody, rowDesc, apiFields, containerFields, errors, warnings);

        if (containerFields.Count > 1)
            errors.Add("Only one container field is supported");
        if (isHeader && (containerFields.Count > 0))
            errors.Add("Container field is only allowed on detail");

        // output errors for fields that were not found in the json
        foreach (string f in apiFields)
        {
            errors.Add("Missing field on " + rowDesc + ": " + f);
        }

        // if a Details container if found, process it by recursion
        if (detailsArray is not null)
        {
            int i = 1;
            foreach (JToken detail in detailsArray)
            {
                ValidateJSONFields(detail, i, metaData, errors, warnings);
                i += 1;
            }
        }
    }

    // this method is for validating the special container fields (only used for stratus biorad)
    // the container fields are used to collapse a 3 tier structure into the 2 tier used in sharepoint
    protected void ValidateJSONContainerFields(JToken jsonbody, string rowDesc, List<string> apiFields, List<string> containerFields, List<string> errors, List<string> warnings)
    {
        // process container fields, ex: Points
        foreach (string containerField in containerFields)
        {
            // get the json data for this container field, ex: Points
            JProperty contJP = jsonbody.Children<JProperty>().Where(jp => string.Equals(jp.Name, containerField, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
            if ((contJP is not null) && (contJP.Value is JArray array))
            {
                JArray contJPArray = array;

                // Get a list of properties in the container field from the json body, but pre-pend the container field name, ex: Points\Position
                List<string> contFieldProperties = new();
                foreach (JToken sfJT in contJPArray.Values<JToken>()) // for each row in the json container
                {
                    foreach (JProperty subJTJP in sfJT.Children<JProperty>()) // for each property for the row
                    {
                        string propname = (containerField + '\\' + subJTJP.Name).ToUpper();
                        if (!contFieldProperties.Contains(propname))
                            contFieldProperties.Add(propname);
                    }
                }

                // Get list of field bindings for this container field, ex: Points\Position, Points\Thickness
                List<string> contFieldBindings = apiFields.Where(f => f.StartsWith(containerField, StringComparison.OrdinalIgnoreCase)).Select(f => f.ToUpper()).ToList();
                foreach (string contFieldBinding in contFieldBindings)
                {
                    // check if the jsonbody has this property in the container field
                    if (contFieldProperties.Contains(contFieldBinding))
                    {
                        _ = contFieldProperties.Remove(contFieldBinding); // remove from the list of properties so we know it was found
                        _ = apiFields.Remove(contFieldBinding); // remove from the list of missing fields
                    }
                    else
                    {
                        errors.Add("Missing field on " + rowDesc + ": " + contFieldBinding);
                    }
                }

                // Output warnings for extra properties in the container field
                foreach (string contFieldProp in contFieldProperties)
                {
                    warnings.Add("Extra field on " + rowDesc + ": " + contFieldProperties);
                }
            }
            else
            {
                errors.Add("Missing container field on " + rowDesc + ": " + containerField);
            }
        }
    }
}