using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using System.Threading;

namespace Creditcall.ChipDna.Client
{
    class ClientApp
    {
        private const int ErrorExit = 1;
        private const int SuccessExit = 0;
        private const string ConfigFilename = "client.config.xml";
        private const int DefaultPort = 1869;
        private const string DefaultIpAddress = "127.0.0.1";
        private const bool DefaultSaveReceipt = false;

        private static Client client;

        private static int Main()
        {
            try
            {
                var settings = new ConfigFileParser(GetAbsolutePath(ConfigFilename));
                string tid = settings.TerminalId;
                string apiKey = settings.ApiKey;
                string posId = settings.PosId;
                string address = settings.ConnectAddress ?? DefaultIpAddress;
                string sslHost = settings.SslHostName;
                bool saveReceipt = settings.SaveReceipt ?? DefaultSaveReceipt;

                int port = DefaultPort;
                if (!string.IsNullOrEmpty(settings.ConnectionPort))
                {
                    if (!int.TryParse(settings.ConnectionPort, out port))
                    {
                        Console.WriteLine("Invalid port number in config.");
                        return ErrorExit;
                    }
                }

                string identifier = !string.IsNullOrEmpty(tid) ? tid :
                                    !string.IsNullOrEmpty(posId) ? posId : null;

                if (identifier == null || (string.IsNullOrEmpty(apiKey) && string.IsNullOrEmpty(tid)))
                {
                    Console.WriteLine("Missing valid credentials.");
                    return ErrorExit;
                }

                Console.WriteLine($"Starting ChipDNAClient with ID={identifier}, Address={address}:{port}");

                client = new Client(identifier, address, port, sslHost, saveReceipt, settings);

                StartHttpServer();

                return SuccessExit;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Startup failed: " + ex.Message);
                return ErrorExit;
            }
        }

        private static void StartHttpServer()
        {
            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://127.0.0.1:18181/start-transaction/");
            listener.Start();

            Console.WriteLine("Listening on:");
            Console.WriteLine("  http://127.0.0.1:18181/start-transaction/");

            while (true)
            {
                HttpListenerContext context = listener.GetContext();
                HttpListenerRequest request = context.Request;
                string rawUrl = request.Url.AbsolutePath;

                try
                {
                    if (request.HttpMethod != "POST")
                    {
                        context.Response.StatusCode = 405;
                        context.Response.Close();
                        continue;
                    }

                    if (rawUrl.StartsWith("/start-transaction", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleStartTransaction(context);
                    }
                    else
                    {
                        context.Response.StatusCode = 404;
                        context.Response.Close();
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Unhandled server error: " + ex.Message);
                    try
                    {
                        context.Response.StatusCode = 500;
                        context.Response.Close();
                    }
                    catch { }
                }
            }
        }

        private static void HandleStartTransaction(HttpListenerContext context)
        {
            const int transactionTimeoutSeconds = 300; 

            try
            {
                var serializer = new XmlSerializer(typeof(TransactionPayload));
                TransactionPayload payload = (TransactionPayload)serializer.Deserialize(context.Request.InputStream);

                Console.WriteLine($"Received start transaction: Amount={payload.amount}, Type={payload.transactionType}");

                client.TransactionCompletionSource = new TaskCompletionSource<Dictionary<string, string>>();

                // start transaction (non-blocking)
                client.PerformStartTransaction(payload.amount, payload.transactionType);

                // Wait for completion with timeout
                var task = client.TransactionCompletionSource.Task;
                if (!task.Wait(TimeSpan.FromSeconds(transactionTimeoutSeconds)))
                {
                    // timeout
                    var errorMap = new Dictionary<string, string>
                    {
                        { ParameterKeys.TransactionResult, "TIMEOUT" },
                        { ParameterKeys.Errors, "Transaction timed out waiting for device response" }
                    };

                    var wrappedTimeout = new SerializableKeyValueList(errorMap);
                    context.Response.ContentType = "application/xml";
                    new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrappedTimeout);
                    return;
                }

                // Task completed — handle possible exception
                Dictionary<string, string> transactionResult;
                try
                {
                    transactionResult = task.Result; // safe now, Wait returned
                }
                catch (AggregateException agg)
                {
                    var first = agg.Flatten().InnerExceptions[0];
                    var errMap = new Dictionary<string, string>
                    {
                        { ParameterKeys.TransactionResult, "ERROR" },
                        { ParameterKeys.Errors, $"Transaction task faulted: {first.Message}" }
                    };
                    var wrapped = new SerializableKeyValueList(errMap);
                    context.Response.ContentType = "application/xml";
                    new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrapped);
                    return;
                }

                // If it's a SALE and approved, call ConfirmTransaction and attach its XML
                transactionResult.TryGetValue(ParameterKeys.TransactionType, out string txType);
                transactionResult.TryGetValue(ParameterKeys.TransactionResult, out string txResult);
                transactionResult.TryGetValue(ParameterKeys.Reference, out string txReference);

                if (!string.IsNullOrEmpty(txType) && txType.Equals("SALE", StringComparison.OrdinalIgnoreCase) &&
                    !string.IsNullOrEmpty(txResult) && txResult.Equals("APPROVED", StringComparison.OrdinalIgnoreCase) &&
                    !string.IsNullOrEmpty(txReference))
                {
                    try
                    {
                        TransactionConfirmation confirmation;
                        client.PerformConfirmTransaction(txReference, out confirmation);

                        // Keep original fields, but also add confirmation-prefixed fields
                        transactionResult["CONFIRM_RESULT"] = confirmation?.Result ?? "";
                        transactionResult["CONFIRM_ERRORS"] = confirmation?.Errors ?? "";
                        if (!string.IsNullOrEmpty(confirmation.ErrorDescription)) 
                        { 
                            transactionResult["CONFIRM_ERRORS"] = $"{confirmation.Errors} {confirmation.ErrorDescription}"; 
                        }
                        if (!string.IsNullOrEmpty(confirmation?.ReceiptDataCardholder))
                        {
                            transactionResult[ParameterKeys.ReceiptDataCardholder] = confirmation.ReceiptDataCardholder;
                        }

                        // XML (omit declaration so it can be embedded)
                        var xml = SerializeToXml(confirmation, omitXmlDeclaration: true);
                        transactionResult["TRANSACTION_CONFIRMATION"] = xml;
                    }
                    catch (Exception ex)
                    {
                        // preserve original transaction result but add an error key
                        transactionResult["CONFIRM_RESULT"] = "ERROR";
                        transactionResult["CONFIRM_ERRORS"] = $"ConfirmTransaction failed: {ex.Message}";
                    }
                }

                // Return the final key/value list as before
                var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList));
                context.Response.ContentType = "application/xml";
                var wrappedResult = new SerializableKeyValueList(transactionResult);
                responseSerializer.Serialize(context.Response.OutputStream, wrappedResult);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in start-transaction: " + ex.Message);
                context.Response.StatusCode = 500;
            }
            finally
            {
                context.Response.OutputStream.Close();
            }
        }

        private static string GetAbsolutePath(string fileName)
        {
            var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            return Path.Combine(path ?? "", fileName);
        }

        private static string EscapeForJson(string input)
        {
            return input?.Replace("\\", "\\\\")
                         .Replace("\"", "\\\"")
                         .Replace("\n", "\\n")
                         .Replace("\r", "\\r");
        }

        private static string SerializeToJson(Dictionary<string, string> dict)
        {
            var sb = new StringBuilder();
            sb.Append("{");

            foreach (var kv in dict)
            {
                sb.Append($"\"{EscapeForJson(kv.Key)}\":\"{EscapeForJson(kv.Value)}\",");
            }

            if (sb.Length > 1)
                sb.Length--; // remove last comma

            sb.Append("}");
            return sb.ToString();
        }

        // helper to serialize an object to an XML string (no namespaces)
        private static string SerializeToXml<T>(T obj, bool omitXmlDeclaration = true)
        {
            if (obj == null) return string.Empty;
            var xs = new XmlSerializer(typeof(T));
            var ns = new XmlSerializerNamespaces();
            ns.Add(string.Empty, string.Empty); // remove xsi/xsd namespaces

            var settings = new XmlWriterSettings
            {
                OmitXmlDeclaration = omitXmlDeclaration,
                Indent = false,
                Encoding = Encoding.UTF8
            };

            using (var sw = new StringWriter())
            using (var xw = XmlWriter.Create(sw, settings))
            {
                xs.Serialize(xw, obj, ns);
                return sw.ToString();
            }
        }
    }

    [XmlRoot("TransactionResult")]
    public class SerializableKeyValueList
    {
        [XmlElement("Entry")]
        public List<Entry> Items { get; set; }

        public SerializableKeyValueList() { }

        public SerializableKeyValueList(Dictionary<string, string> dict)
        {
            Items = new List<Entry>();
            foreach (var kv in dict)
            {
                Items.Add(new Entry { Key = kv.Key, Value = kv.Value });
            }
        }
    }

    public class Entry
    {
        [XmlElement("Key")]
        public string Key { get; set; }

        [XmlElement("Value")]
        public string Value { get; set; }
    }

    [XmlRoot("TransactionPayload")]
    public class TransactionPayload
    {
        public string amount { get; set; }
        public string transactionType { get; set; }

        // For confirm transaction
        public string transactionReference { get; set; }
    }

    [XmlRoot("TransactionConfirmation")]
    public class TransactionConfirmation
    {
        public string Result { get; set; }
        public string Errors { get; set; }
        public string ErrorDescription { get; set; }
        public string ReceiptDataCardholder { get; set; }

    }
}
