using System.Text.RegularExpressions;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.Extensions.FileProviders;
using static Fast.Models.Enums;

namespace Fast.Logic;

public abstract partial class DocBaseOpenXml : IDisposable
{
    protected readonly Dictionary<string, List<Run>> docRunsByKey = new(StringComparer.OrdinalIgnoreCase);
    protected readonly Document doc;
    protected readonly WordprocessingDocument wordprocessingDocument;
    protected readonly MainDocumentPart mainDocumentPart;

    protected readonly MyDbContext db;
    protected readonly string filesFolderPath;
    protected readonly string templatesFolderPath;
    protected readonly string signaturesFolderPath;
    protected readonly string logosFolderPath;
    private readonly MemoryStream documentMemoryStream;
    private bool disposed = false;

    public DocBaseOpenXml(string filesFolderPath, string templatesFolderPath, string signaturesFolderPath, string logosFolderPath, bool ignoreMissingKeys)
    {
        db = Main.CreateContext();
        this.filesFolderPath = filesFolderPath;
        this.templatesFolderPath = templatesFolderPath;
        this.signaturesFolderPath = signaturesFolderPath;
        this.logosFolderPath = logosFolderPath;

        using var provider = new PhysicalFileProvider(templatesFolderPath);
        var fileInfo = provider.GetFileInfo(TemplateFileName);

        if (!fileInfo.Exists || string.IsNullOrEmpty(fileInfo.PhysicalPath))
        {
            throw new FileNotFoundException($"Template file '{TemplateFileName}' not found or is inaccessible.", fileInfo.PhysicalPath ?? TemplateFileName);
        }

        documentMemoryStream = new MemoryStream();
        using (var templateStream = fileInfo.CreateReadStream())
        {
            templateStream.CopyTo(this.documentMemoryStream);
            documentMemoryStream.Position = 0;
        }

        wordprocessingDocument = WordprocessingDocument.Open(this.documentMemoryStream, true);
        mainDocumentPart = wordprocessingDocument.MainDocumentPart!;
        doc = mainDocumentPart.Document;

        var docKeys = GetDocKeys();
        FillDocKeyRuns(docKeys, ignoreMissingKeys);
    }

    protected abstract HashSet<string> GetDocKeys();

    protected abstract string TemplateFileName { get; }

    public void SetText(string key, string textToSet, bool removeRowIfEmptyText = false)
    {
        textToSet ??= string.Empty;

        if (!docRunsByKey.TryGetValue(key, out List<Run>? runsForKey))
            return;

        var tagToReplace = $"«{key}»";
        foreach (var run in runsForKey)
        {
            var textElementsInRun = run.Elements<Text>().ToList();
            if (textElementsInRun.Count != 0)
            {
                foreach (var textElement in textElementsInRun)
                {
                    if (textElement.Text != null && textElement.Text.Contains(tagToReplace, StringComparison.OrdinalIgnoreCase))
                        textElement.Text = textElement.Text.Replace(tagToReplace, textToSet, StringComparison.OrdinalIgnoreCase);
                }
            }

            if (removeRowIfEmptyText && string.IsNullOrWhiteSpace(textToSet))
                RemoveParentRowOfRun(run);
        }
    }

    protected static void SetTableCellText(
        TableCell tableCell,
        string text,
        bool bold = false,
        JustificationValues? alignment = null)
    {
        // find or create the first paragraph in the cell
        var paragraph = tableCell.Elements<Paragraph>().FirstOrDefault();
        if (paragraph == null)
        {
            paragraph = new Paragraph();
            tableCell.AppendChild(paragraph);
        }

        // find or create the first run in the paragraph
        var run = paragraph.Elements<Run>().FirstOrDefault();
        if (run == null)
        {
            run = new Run();
            paragraph.AppendChild(run);
        }

        // set paragraph alignment if specified
        if (alignment != null)
        {
            var paragraphProperties = paragraph.Elements<ParagraphProperties>().FirstOrDefault();
            if (paragraphProperties == null)
            {
                paragraphProperties = new ParagraphProperties();
                paragraph.PrependChild(paragraphProperties);
            }

            var justification = paragraphProperties.Elements<Justification>().FirstOrDefault();
            if (justification == null)
            {
                justification = new Justification();
                paragraphProperties.AppendChild(justification);
            }
            justification.Val = alignment.Value;
        }

        // set bold formatting if specified
        if (bold)
        {
            var runProperties = run.Elements<RunProperties>().FirstOrDefault();
            if (runProperties == null)
            {
                runProperties = new RunProperties();
                run.PrependChild(runProperties);
            }

            var boldElement = runProperties.Elements<Bold>().FirstOrDefault();
            if (boldElement == null)
            {
                boldElement = new Bold();
                runProperties.AppendChild(boldElement);
            }
        }

        // set text content
        text ??= "";
        var textElement = run.Elements<Text>().FirstOrDefault();
        if (textElement == null)
        {
            textElement = new Text();
            run.AppendChild(textElement);
        }
        textElement.Text = text;
    }

    private static void RemoveParentRowOfRun(Run run)
    {
        var parentRow = GetParentRowOfRun(run);
        parentRow?.Remove();
    }

    private static TableRow? GetParentRowOfRun(Run run)
    {
        var parentNode = run.Parent;
        while (parentNode != null)
        {
            if (parentNode is TableRow tableRow)
            {
                return tableRow;
            }
            parentNode = parentNode.Parent;
        }

        return null;
    }

    protected void SetImage(string? filePath, string imageNameOrTitle)
    {
        string? actualFilePath = GetFilePathOnDisk(filePath);
        if (string.IsNullOrEmpty(actualFilePath) || string.IsNullOrEmpty(imageNameOrTitle) || mainDocumentPart?.Document == null)
            return;

        var matchingDrawing = (
            from drawing in mainDocumentPart.Document.Descendants<Drawing>()
            let docProps = drawing.Descendants<DocumentFormat.OpenXml.Drawing.Wordprocessing.DocProperties>().FirstOrDefault()
            where docProps?.Name?.Value == imageNameOrTitle || docProps?.Title?.Value == imageNameOrTitle
            select drawing
        ).FirstOrDefault();

        if (matchingDrawing != null)
            ReplaceImageInDrawing(matchingDrawing, actualFilePath);
    }

    private void ReplaceImageInDrawing(Drawing drawing, string imageFilePath)
    {
        var blip = drawing.Descendants<DocumentFormat.OpenXml.Drawing.Blip>().FirstOrDefault();
        var imagePartId = blip?.Embed?.Value;
        if (string.IsNullOrEmpty(imagePartId))
            return;

        OpenXmlPart? part = null;
        try
        {
            part = mainDocumentPart.GetPartById(imagePartId);
        }
        catch
        {
            // GetPartById() will throw if it can't find the part ID.
            // do nothing if it can't find it.
        }

        if (part == null)
            return;

        if (part is ImagePart imagePart)
        {
            // replace the content of the existing ImagePart
            using var imageStream = new FileStream(imageFilePath, FileMode.Open);
            imagePart.FeedData(imageStream);
        }
    }

    /// <summary>
    /// Gets the file path on disk by searching in a case insensitve manner
    /// On Linux the file name and extension are case sensitive and may not match the case of the filePath parameter passed in
    /// This method will return the file path with the case that it actually is on disk, regardless of the case of the filePath parameter passed in
    /// </summary>
    protected string? GetFilePathOnDisk(string? filePath)
    {
        if (filePath == null)
            return null;

        var parentDirectory = Directory.GetParent(filePath)?.FullName;
        if (parentDirectory == null)
            return null;

        var files = Directory.GetFiles(parentDirectory, "*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive });
        if (files.Length == 0)
            return null;

        var filePathOnDisk = files.Where(f => f.Contains(filePath, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
        var isSignaturePath = filePath.Contains(signaturesFolderPath, StringComparison.OrdinalIgnoreCase);
        if (filePathOnDisk == null && isSignaturePath)
        {
            var sampleSigPath = Path.Join(signaturesFolderPath, "Sample.jpg");
            if (File.Exists(sampleSigPath))
                filePathOnDisk = sampleSigPath;
        }

        return filePathOnDisk;
    }

    protected static string FormatNum(decimal? num, NumFormat format)
    {
        if (!num.HasValue)
            return "";

        //manually specifying the formats
        //otherwise NumberFormatInfo may vary with OS and OS settings which result in different formats
        string result = format switch
        {
            NumFormat.n0 => num.Value.ToString("#,##0;(#,##0);0"),
            NumFormat.n1 => num.Value.ToString("#,##0.0;(#,##0.0);0.0"),
            NumFormat.n2 => num.Value.ToString("#,##0.00;(#,##0.00);0.00"),
            NumFormat.n3 => num.Value.ToString("#,##0.000;(#,##0.000);0.000"),
            NumFormat.n4 => num.Value.ToString("#,##0.0000;(#,##0.0000);0.0000"),
            NumFormat.n5 => num.Value.ToString("#,##0.00000;(#,##0.00000);0.00000"),
            NumFormat.c2 => num.Value.ToString("$ #,##0.00;$ (#,##0.00);$ 0.00"),
            NumFormat.c3 => num.Value.ToString("$ #,##0.000;$ (#,##0.000);$ 0.000"),
            NumFormat.c4 => num.Value.ToString("$ #,##0.0000;$ (#,##0.0000);$ 0.0000"),
            NumFormat.c5 => num.Value.ToString("$ #,##0.00000;$ (#,##0.00000);$ 0.00000"),
            NumFormat.p2 => num.Value.ToString("#,##0.00%;(#,##0.00%);0.00%"),
            NumFormat.p3 => num.Value.ToString("#,##0.000%;(#,##0.000%);0.000%"),
            NumFormat.p4 => num.Value.ToString("#,##0.0000%;(#,##0.0000%);0.0000%"),
            NumFormat.p5 => num.Value.ToString("#,##0.00000%;(#,##0.00000%);0.00000%"),
            NumFormat.p6 => num.Value.ToString("#,##0.000000%;(#,##0.000000%);0.000000%"),
            _ => throw new Exception($"Undefined format code: {format}")
        };

        return result;
    }

    [GeneratedRegex("«.*?»")]
    private static partial Regex TagRegex();
    private void FillDocKeyRuns(HashSet<string> keysToFind, bool ignoreMissingKeys)
    {
        if (mainDocumentPart?.Document?.Body == null)
            return;

        // process elements in the main document body
        var bodyParagraphs = mainDocumentPart.Document.Body.Descendants<Paragraph>();
        if (bodyParagraphs != null)
            ProcessParagraphsForKeys(bodyParagraphs, keysToFind);

        // process elements in headers
        foreach (var headerPart in mainDocumentPart.HeaderParts)
        {
            var paragraphs = headerPart.Header?.Descendants<Paragraph>();
            if (paragraphs != null)
                ProcessParagraphsForKeys(paragraphs, keysToFind);
        }

        // process elements in footers
        foreach (var footerPart in mainDocumentPart.FooterParts)
        {
            var paragraphs = footerPart.Footer?.Descendants<Paragraph>();
            if (paragraphs != null)
                ProcessParagraphsForKeys(paragraphs, keysToFind);
        }

        if (!ignoreMissingKeys)
        {
            foreach (string key in keysToFind)
            {
                if (!docRunsByKey.ContainsKey(key))
                    throw new Exception($"Could not find key {{{key}}} in template file.");
            }
        }
    }

    private void ProcessParagraphsForKeys(IEnumerable<Paragraph> paragraphs, HashSet<string> keysToFind)
    {
        // combine tags that might be split across multiple runs
        var allRuns = paragraphs.SelectMany(x => x.Elements<Run>()).ToList();
        CombineRunTags(allRuns);

        foreach (var paragraph in paragraphs)
        {
            var runsInParagraph = paragraph.Elements<Run>().ToList();
            if (runsInParagraph.Count == 0)
                continue;

            foreach (var run in runsInParagraph)
            {
                var runText = GetRunTextContent(run);
                if (string.IsNullOrEmpty(runText))
                    continue;

                var runKeyMatches = TagRegex().Matches(runText);
                foreach (var match in runKeyMatches.Cast<Match>())
                {
                    var runKey = match.Value.Replace("«", string.Empty).Replace("»", string.Empty);
                    if (keysToFind.Contains(runKey))
                    {
                        if (!docRunsByKey.ContainsKey(runKey))
                        {
                            docRunsByKey.Add(runKey, new List<Run>());
                        }
                        docRunsByKey[runKey].Add(run);
                    }
                }
            }
        }
    }

    //sometimes tags are split between runs
    //this combines runs that were split into a single run
    //this makes it easier to replace a tag with new text
    //the old runs are removed
    protected static void CombineRunTags(List<Run> runs)
    {
        Run? replacementRun = null;
        var runsToRemove = new List<Run>();

        foreach (var run in runs)
        {
            var runTextElement = GetFirstTextElement(run);
            var runText = runTextElement.Text ?? string.Empty;

            //if begin tag but no end tag after it
            var lastIndexOfBeginTag = runText.LastIndexOf('«');
            var lastIndexOfEndTag = runText.LastIndexOf('»');
            if (lastIndexOfBeginTag > lastIndexOfEndTag)
            {
                replacementRun = run; //set this run as the replacement run
            }
            //if we are mid-tag
            else if (replacementRun != null && !runText.Contains('»'))
            {
                var replacementRunTextElement = GetFirstTextElement(replacementRun);
                replacementRunTextElement.Text += runText; //combine the text of this run with the previous run
                runsToRemove.Add(run);
            }
            //if end tag without begin tag
            else if (replacementRun != null && runText.Contains('»'))
            {
                var replacementRunTextElement = GetFirstTextElement(replacementRun);
                replacementRunTextElement.Text += runText; //combine the text of this run with the previous run
                runsToRemove.Add(run);
                replacementRun = null; //reset the replacement run since we found an end tag
            }
        }

        foreach (var runToRemove in runsToRemove)
        {
            runToRemove.Remove();
        }
    }

    private static Text GetFirstTextElement(Run run)
    {
        var textElement = run.Elements<Text>().FirstOrDefault();
        if (textElement == null)
        {
            textElement = new Text();
            run.AppendChild(textElement);
        }
        return textElement;
    }

    private static string GetRunTextContent(Run run)
    {
        if (run == null)
            return string.Empty;

        var sb = new StringBuilder();
        var textElements = run.Elements<Text>();
        foreach (var textElement in textElements)
        {
            if (textElement?.Text != null)
                sb.Append(textElement.Text);
        }
        return sb.ToString();
    }

    /// <summary>
    /// Writes an exception text file to disk.
    /// </summary>
    /// <returns>Returns the file name of the text file that was created.  This does not include the full path.</returns>
    protected string WriteExceptionToFile(Exception exception, string dealNum)
    {
        var fileName = Util.String.GetNewGuid() + ".txt";
        var fullPath = Path.Join(filesFolderPath, fileName);

        var errorText = GetErrorText(exception, dealNum);

        System.IO.File.WriteAllText(fullPath, errorText);
        return fileName;
    }

    private static string GetErrorText(Exception exception, string dealNum)
    {
        var messagePrefix = $"Deal #: {dealNum}{Environment.NewLine}";
        var errorMsg = Util.String.GetExceptionMessage(exception);
        return messagePrefix + errorMsg;
    }

    /// <summary>
    /// Saves the modified document to the specified file path.
    /// </summary>
    /// <param name="outputFilePath">The full path where the document should be saved.</param>
    public void Save(string outputFilePath)
    {
        ObjectDisposedException.ThrowIf(disposed, this);
        if (wordprocessingDocument == null)
            throw new InvalidOperationException("document is not loaded or has been disposed.");

        // flush changes to memory stream
        mainDocumentPart?.Document.Save();
        wordprocessingDocument.Save();

        // save the package to a new file path
        using var clonedDocument = wordprocessingDocument.Clone(outputFilePath);
        clonedDocument.Save();
    }

    public MemoryStream SaveToMemoryStream()
    {
        ObjectDisposedException.ThrowIf(disposed, this);
        if (wordprocessingDocument == null)
            throw new InvalidOperationException("document is not loaded or has been disposed.");

        // flush changes to memory stream
        mainDocumentPart?.Document.Save();
        wordprocessingDocument.Save();

        // clone to a new MemoryStream
        var outputStream = new MemoryStream();
        using (var clonedDocument = wordprocessingDocument.Clone(outputStream))
        {
            clonedDocument.Save();
        }
        outputStream.Position = 0;
        return outputStream;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                documentMemoryStream?.Dispose();
                wordprocessingDocument?.Dispose();
            }
            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

}
