using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Packaging.Models;
using Umbraco.Core.Services;
using umbraco.interfaces;
using File = System.IO.File;

namespace Umbraco.Core.Packaging
{
    internal class PackageInstallation : IPackageInstallation
    {
        private readonly IFileService _fileService;
        private readonly IMacroService _macroService;
        private readonly IPackagingService _packagingService;
        private IConflictingPackageData _conflictingPackageData;
        private readonly IPackageExtraction _packageExtraction;
        private string _fullPathToRoot;

        public PackageInstallation(IPackagingService packagingService, IMacroService macroService,
            IFileService fileService, IPackageExtraction packageExtraction)
            : this(packagingService, macroService, fileService, packageExtraction, GlobalSettings.FullpathToRoot)
        {}

        public PackageInstallation(IPackagingService packagingService, IMacroService macroService,
            IFileService fileService, IPackageExtraction packageExtraction, string fullPathToRoot)
        {
            if (packageExtraction != null) _packageExtraction = packageExtraction; 
            else throw new ArgumentNullException("packageExtraction");
            
            if (macroService != null) _macroService = macroService;
            else throw new ArgumentNullException("macroService");
            
            if (fileService != null) _fileService = fileService;
            else throw new ArgumentNullException("fileService");

            if (packagingService != null) _packagingService = packagingService;
            else throw new ArgumentNullException("packagingService");

            _fullPathToRoot = fullPathToRoot;
        }
        
        public IConflictingPackageData ConflictingPackageData
        {
            private get
            {
                return _conflictingPackageData ??
                       (_conflictingPackageData = new ConflictingPackageData(_macroService, _fileService));
            }
            set
            {
                if (_conflictingPackageData != null)
                {
                    throw new PropertyConstraintException("This property already have a value");
                }
                _conflictingPackageData = value;
            }
        }

        public string FullPathToRoot
        {
            private get { return _fullPathToRoot; }
            set
            {

                if (_fullPathToRoot != null)
                {
                    throw new PropertyConstraintException("This property already have a value");
                }

                _fullPathToRoot = value;
            }
        }
        
        public MetaData GetMetaData(string packageFilePath)
        {
            try
            {
                XElement rootElement = GetConfigXmlElement(packageFilePath);
                return GetMetaData(rootElement);
            }
            catch (Exception e)
            {
                throw new Exception("Error reading " + packageFilePath, e);
            }
        }

        public PreInstallWarnings GetPreInstallWarnings(string packageFilePath)
        {
            try
            {
                XElement rootElement = GetConfigXmlElement(packageFilePath);
                return GetPreInstallWarnings(packageFilePath, rootElement);
            }
            catch (Exception e)
            {
                throw new Exception("Error reading " + packageFilePath, e);
            }
        }

        public InstallationSummary InstallPackage(string packageFile, int userId)
        {
            XElement dataTypes;
            XElement languages;
            XElement dictionaryItems;
            XElement macroes;
            XElement files;
            XElement templates;
            XElement documentTypes;
            XElement styleSheets;
            XElement documentSet;
            XElement documents;
            XElement actions;
            MetaData metaData;
            InstallationSummary installationSummary;
            
            try
            {
                XElement rootElement = GetConfigXmlElement(packageFile);
                PackageSupportedCheck(rootElement);
                PackageStructureSanetyCheck(packageFile, rootElement);
                dataTypes = rootElement.Element(Constants.Packaging.DataTypesNodeName);
                languages = rootElement.Element(Constants.Packaging.LanguagesNodeName);
                dictionaryItems = rootElement.Element(Constants.Packaging.DictionaryItemsNodeName);
                macroes = rootElement.Element(Constants.Packaging.MacrosNodeName);
                files = rootElement.Element(Constants.Packaging.FilesNodeName);
                templates = rootElement.Element(Constants.Packaging.TemplatesNodeName);
                documentTypes = rootElement.Element(Constants.Packaging.DocumentTypesNodeName);
                styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName);
                documentSet = rootElement.Element(Constants.Packaging.DocumentSetNodeName);
                documents = rootElement.Element(Constants.Packaging.DocumentsNodeName);
                actions = rootElement.Element(Constants.Packaging.ActionsNodeName);

                metaData = GetMetaData(rootElement);
                installationSummary = new InstallationSummary {MetaData = metaData};
            }
            catch (Exception e)
            {
                throw new Exception("Error reading " + packageFile, e);
            }

            try
            {
                var dataTypeDefinitions = EmptyEnumerableIfNull<IDataTypeDefinition>(dataTypes) ?? InstallDataTypes(dataTypes, userId);
                installationSummary.DataTypesInstalled = dataTypeDefinitions;

                var languagesInstalled = EmptyEnumerableIfNull<ILanguage>(languages) ?? InstallLanguages(languages, userId);
                installationSummary.LanguagesInstalled = languagesInstalled;

                var dictionaryInstalled = EmptyEnumerableIfNull<IDictionaryItem>(dictionaryItems) ?? InstallDictionaryItems(dictionaryItems);
                installationSummary.DictionaryItemsInstalled = dictionaryInstalled;

                var macros = EmptyEnumerableIfNull<IMacro>(macroes) ?? InstallMacros(macroes, userId);
                installationSummary.MacrosInstalled = macros;

                var keyValuePairs = EmptyEnumerableIfNull<string>(packageFile) ?? InstallFiles(packageFile, files);
                installationSummary.FilesInstalled = keyValuePairs;

                var templatesInstalled = EmptyEnumerableIfNull<ITemplate>(templates) ?? InstallTemplats(templates, userId);
                installationSummary.TemplatesInstalled = templatesInstalled;

                var documentTypesInstalled = EmptyEnumerableIfNull<IContentType>(documentTypes) ?? InstallDocumentTypes(documentTypes, userId);
                installationSummary.ContentTypesInstalled =documentTypesInstalled;

                var stylesheetsInstalled = EmptyEnumerableIfNull<IFile>(styleSheets) ?? InstallStylesheets(styleSheets, userId);
                installationSummary.StylesheetsInstalled = stylesheetsInstalled;

                var documentsInstalled = documents != null ? InstallDocuments(documents, userId) 
                    : EmptyEnumerableIfNull<IContent>(documentSet) 
                    ?? InstallDocuments(documentSet, userId);
                installationSummary.ContentInstalled = documentsInstalled;

                var packageActions = EmptyEnumerableIfNull<PackageAction>(actions) ?? GetPackageActions(actions, metaData.Name);
                installationSummary.Actions = packageActions;

                installationSummary.PackageInstalled = true;

                return installationSummary;
            }
            catch (Exception e)
            {
                throw new Exception("Error installing package " + packageFile, e);
            }
        }

        /// <summary>
        /// Temperary check to test that we support stylesheets
        /// </summary>
        /// <param name="rootElement"></param>
        private void PackageSupportedCheck(XElement rootElement)
        {
            XElement styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName);
            if (styleSheets != null && styleSheets.Elements().Any())
                throw new NotSupportedException("Stylesheets is not suported in this version of umbraco");
        }

        private static T[] EmptyArrayIfNull<T>(object obj)
        {
            return obj == null ? new T[0] : null;
        }

        private static IEnumerable<T> EmptyEnumerableIfNull<T>(object obj)
        {
            return obj == null ? Enumerable.Empty<T>() : null;
        }

        private XDocument GetConfigXmlDoc(string packageFilePath)
        {
            string filePathInPackage;
            string configXmlContent = _packageExtraction.ReadTextFileFromArchive(packageFilePath,
                Constants.Packaging.PackageXmlFileName, out filePathInPackage);

            return XDocument.Parse(configXmlContent);
        }

        public XElement GetConfigXmlElement(string packageFilePath)
        {
            XDocument document = GetConfigXmlDoc(packageFilePath);
            if (document.Root == null ||
                document.Root.Name.LocalName.Equals(Constants.Packaging.UmbPackageNodeName) == false)
            {
                throw new ArgumentException("xml does not have a root node called \"umbPackage\"", packageFilePath);
            }
            return document.Root;
        }

        internal void PackageStructureSanetyCheck(string packageFilePath)
        {
            XElement rootElement = GetConfigXmlElement(packageFilePath);
            PackageStructureSanetyCheck(packageFilePath, rootElement);
        }

        private void PackageStructureSanetyCheck(string packageFilePath, XElement rootElement)
        {
            XElement filesElement = rootElement.Element(Constants.Packaging.FilesNodeName);
            if (filesElement != null)
            {
                var sourceDestination = ExtractSourceDestinationFileInformation(filesElement).ToArray();

                var missingFiles = _packageExtraction.FindMissingFiles(packageFilePath, sourceDestination.Select(i => i.Key)).ToArray();

                if (missingFiles.Any())
                {
                    throw new Exception("The following file(s) are missing in the package: " +
                                        string.Join(", ", missingFiles.Select(
                                            mf =>
                                            {
                                                var sd = sourceDestination.Single(fi => fi.Key == mf);
                                                return string.Format("source: \"{0}\" destination: \"{1}\"",
                                                    sd.Key, sd.Value);
                                            })));
                }

                IEnumerable<string> dubletFileNames = _packageExtraction.FindDubletFileNames(packageFilePath).ToArray();

                if (dubletFileNames.Any())
                {
                    throw new Exception("The following filename(s) are found more than one time in the package, since the filename is used ad primary key, this is not allowed: " +
                                        string.Join(", ", dubletFileNames));
                }
            }
        }

        private static IEnumerable<PackageAction> GetPackageActions(XElement actionsElement, string packageName)
        {
            if (actionsElement == null) { return new PackageAction[0]; }

            if (string.Equals(Constants.Packaging.ActionsNodeName, actionsElement.Name.LocalName) == false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.ActionsNodeName + "\" as root",
                    "actionsElement");
            }

            return actionsElement.Elements(Constants.Packaging.ActionNodeName)
                .Select(elemet =>
                {
                    XAttribute aliasAttr = elemet.Attribute(Constants.Packaging.AliasNodeNameCapital);
                    if (aliasAttr == null)
                        throw new ArgumentException(
                            "missing \"" + Constants.Packaging.AliasNodeNameCapital + "\" atribute in alias element",
                            "actionsElement");

                    var packageAction = new PackageAction
                    {
                        XmlData = elemet, 
                        Alias = aliasAttr.Value,
                        PackageName = packageName,
                    };


                    XAttribute attr = elemet.Attribute(Constants.Packaging.RunatNodeAttribute);

                    ActionRunAt runAt;
                    if (attr != null && Enum.TryParse(attr.Value, true, out runAt)) { packageAction.RunAt = runAt; }

                    attr = elemet.Attribute(Constants.Packaging.UndoNodeAttribute);

                    bool undo;
                    if (attr != null && bool.TryParse(attr.Value, out undo)) { packageAction.Undo = undo; }


                    return packageAction;
                }).ToArray();
        }

        private IEnumerable<IContent> InstallDocuments(XElement documentsElement, int userId = 0)
        {
            if ((string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName) == false)
                && (string.Equals(Constants.Packaging.DocumentsNodeName, documentsElement.Name.LocalName) == false))
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.DocumentsNodeName + "\" as root",
                    "documentsElement");
            }

            if (string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName))
                return _packagingService.ImportContent(documentsElement, -1, userId);

            return
                documentsElement.Elements(Constants.Packaging.DocumentSetNodeName)
                    .SelectMany(documentSetElement => _packagingService.ImportContent(documentSetElement, -1, userId))
                    .ToArray();
        }

        private IEnumerable<IFile> InstallStylesheets(XElement styleSheetsElement, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.StylesheetsNodeName, styleSheetsElement.Name.LocalName) == false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.StylesheetsNodeName + "\" as root",
                    "styleSheetsElement");
            }

            // TODO: Call _packagingService when import stylesheets import has been implimentet
            if (styleSheetsElement.HasElements == false) { return new List<IFile>(); }

            throw new NotImplementedException("The packaging service do not yes have a method for importing stylesheets");
        }

        private IEnumerable<IContentType> InstallDocumentTypes(XElement documentTypes, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.DocumentTypesNodeName, documentTypes.Name.LocalName) == false)
            {
                if (string.Equals(Constants.Packaging.DocumentTypeNodeName, documentTypes.Name.LocalName) == false)
                    throw new ArgumentException(
                        "Must be \"" + Constants.Packaging.DocumentTypesNodeName + "\" as root", "documentTypes");

                documentTypes = new XElement(Constants.Packaging.DocumentTypesNodeName, documentTypes);
            }

            return _packagingService.ImportContentTypes(documentTypes, userId);
        }

        private IEnumerable<ITemplate> InstallTemplats(XElement templateElement, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.TemplatesNodeName, templateElement.Name.LocalName) == false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.TemplatesNodeName + "\" as root",
                    "templateElement");
            }
            return _packagingService.ImportTemplates(templateElement, userId);
        }

        private IEnumerable<string> InstallFiles(string packageFilePath, XElement filesElement)
        {
            var sourceDestination = ExtractSourceDestinationFileInformation(filesElement);
            sourceDestination = AppendRootToDestination(FullPathToRoot, sourceDestination);

            _packageExtraction.CopyFilesFromArchive(packageFilePath, sourceDestination);
            
            return sourceDestination.Select(sd => sd.Value).ToArray();
        }

        private KeyValuePair<string, string>[] AppendRootToDestination(string fullpathToRoot, IEnumerable<KeyValuePair<string, string>> sourceDestination)
        {
            return
                sourceDestination.Select(
                    sd => new KeyValuePair<string, string>(sd.Key, Path.Combine(fullpathToRoot, sd.Value))).ToArray();
        }

        private IEnumerable<IMacro> InstallMacros(XElement macroElements, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.MacrosNodeName, macroElements.Name.LocalName) == false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.MacrosNodeName + "\" as root",
                    "macroElements");
            }
            return _packagingService.ImportMacros(macroElements, userId);
        }

        private IEnumerable<IDictionaryItem> InstallDictionaryItems(XElement dictionaryItemsElement)
        {
            if (string.Equals(Constants.Packaging.DictionaryItemsNodeName, dictionaryItemsElement.Name.LocalName) ==
                false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.DictionaryItemsNodeName + "\" as root",
                    "dictionaryItemsElement");
            }
            return _packagingService.ImportDictionaryItems(dictionaryItemsElement);
        }

        private IEnumerable<ILanguage> InstallLanguages(XElement languageElement, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.LanguagesNodeName, languageElement.Name.LocalName) == false)
            {
                throw new ArgumentException("Must be \"" + Constants.Packaging.LanguagesNodeName + "\" as root", "languageElement");
            }
            return _packagingService.ImportLanguages(languageElement, userId);
        }

        private IEnumerable<IDataTypeDefinition> InstallDataTypes(XElement dataTypeElements, int userId = 0)
        {
            if (string.Equals(Constants.Packaging.DataTypesNodeName, dataTypeElements.Name.LocalName) == false)
            {
                if (string.Equals(Constants.Packaging.DataTypeNodeName, dataTypeElements.Name.LocalName) == false)
                {
                    throw new ArgumentException("Must be \"" + Constants.Packaging.DataTypeNodeName + "\" as root", "dataTypeElements");
                }
            }
            return _packagingService.ImportDataTypeDefinitions(dataTypeElements, userId);
        }

        private PreInstallWarnings GetPreInstallWarnings(string packagePath, XElement rootElement)
        {
            XElement files = rootElement.Element(Constants.Packaging.FilesNodeName);
            XElement styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName);
            XElement templates = rootElement.Element(Constants.Packaging.TemplatesNodeName);
            XElement alias = rootElement.Element(Constants.Packaging.MacrosNodeName);
            
            var sourceDestination = EmptyArrayIfNull<KeyValuePair<string, string>>(files) ?? ExtractSourceDestinationFileInformation(files);

            var installWarnings = new PreInstallWarnings();

            var macroAliases = EmptyEnumerableIfNull<IMacro>(alias) ?? ConflictingPackageData.FindConflictingMacros(alias);
            installWarnings.ConflictingMacroAliases = macroAliases;

            var templateAliases = EmptyEnumerableIfNull<ITemplate>(templates) ?? ConflictingPackageData.FindConflictingTemplates(templates);
            installWarnings.ConflictingTemplateAliases = templateAliases;

            var stylesheetNames = EmptyEnumerableIfNull<IFile>(styleSheets) ?? ConflictingPackageData.FindConflictingStylesheets(styleSheets);
            installWarnings.ConflictingStylesheetNames = stylesheetNames;
            
            installWarnings.UnsecureFiles = FindUnsecureFiles(sourceDestination);
            installWarnings.FilesReplaced = FindFilesToBeReplaced(sourceDestination);
            installWarnings.AssembliesWithLegacyPropertyEditors = FindLegacyPropertyEditors(packagePath, sourceDestination);
            
            return installWarnings;
        }

        private KeyValuePair<string, string>[] FindFilesToBeReplaced(IEnumerable<KeyValuePair<string, string>> sourceDestination)
        {
            return sourceDestination.Where(sd => File.Exists(Path.Combine(FullPathToRoot, sd.Value))).ToArray();
        }

        private IEnumerable<string> FindLegacyPropertyEditors(string packagePath, IEnumerable<KeyValuePair<string, string>> sourceDestinationPair)
        {
            var dlls = sourceDestinationPair.Where(
                sd => (Path.GetExtension(sd.Value) ?? string.Empty).Equals(".dll", StringComparison.InvariantCultureIgnoreCase)).Select(sd => sd.Key).ToArray();

            if (dlls.Any() == false) { return new List<string>(); }
            
            // Now we want to see if the DLLs contain any legacy data types since we want to warn people about that
            string[] assemblyErrors;
            IEnumerable<byte[]> assemblyesToScan =_packageExtraction.ReadFilesFromArchive(packagePath, dlls);
            return PackageBinaryInspector.ScanAssembliesForTypeReference<IDataType>(assemblyesToScan, out assemblyErrors).ToArray();
        }

        private KeyValuePair<string, string>[] FindUnsecureFiles(IEnumerable<KeyValuePair<string, string>> sourceDestinationPair)
        {
            return sourceDestinationPair.Where(sd => IsFileDestinationUnsecure(sd.Value)).ToArray();
        }

        private bool IsFileDestinationUnsecure(string destination)
        {
            var unsecureDirNames = new[] {"bin", "app_code"};
            if(unsecureDirNames.Any(ud => destination.StartsWith(ud, StringComparison.InvariantCultureIgnoreCase)))
                return true;

            string extension = Path.GetExtension(destination);
            return extension != null && extension.Equals(".dll", StringComparison.InvariantCultureIgnoreCase);
        }
        
        private KeyValuePair<string, string>[] ExtractSourceDestinationFileInformation(XElement filesElement)
        {
            if (string.Equals(Constants.Packaging.FilesNodeName, filesElement.Name.LocalName) == false)
            {
                throw new ArgumentException("the root element must be \"Files\"", "filesElement");
            }

            return filesElement.Elements(Constants.Packaging.FileNodeName)
                .Select(e =>
                {
                    XElement guidElement = e.Element(Constants.Packaging.GuidNodeName);
                    if (guidElement == null)
                    {
                        throw new ArgumentException("Missing element \"" + Constants.Packaging.GuidNodeName + "\"",
                            "filesElement");
                    }

                    XElement orgPathElement = e.Element(Constants.Packaging.OrgPathNodeName);
                    if (orgPathElement == null)
                    {
                        throw new ArgumentException("Missing element \"" + Constants.Packaging.OrgPathNodeName + "\"",
                            "filesElement");
                    }

                    XElement orgNameElement = e.Element(Constants.Packaging.OrgNameNodeName);
                    if (orgNameElement == null)
                    {
                        throw new ArgumentException("Missing element \"" + Constants.Packaging.OrgNameNodeName + "\"",
                            "filesElement");
                    }

                    var fileName = PrepareAsFilePathElement(orgNameElement.Value);
                    var relativeDir = UpdatePathPlaceholders(PrepareAsFilePathElement(orgPathElement.Value));

                    var relativePath = Path.Combine(relativeDir, fileName);
                    

                    return new KeyValuePair<string, string>(guidElement.Value, relativePath);
                }).ToArray();
        }

        private static string PrepareAsFilePathElement(string pathElement)
        {
            return pathElement.TrimStart(new[] {'\\', '/', '~'}).Replace("/", "\\");
        }

        private MetaData GetMetaData(XElement xRootElement)
        {
            XElement infoElement = xRootElement.Element(Constants.Packaging.InfoNodeName);

            if (infoElement == null)
            {
                throw new ArgumentException("Did not hold a \"" + Constants.Packaging.InfoNodeName + "\" element",
                    "xRootElement");
            }

            var majorElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsMajorXpath);
            var minorElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsMinorXpath);
            var patchElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsPatchXpath);
            var nameElement = infoElement.XPathSelectElement(Constants.Packaging.PackageNameXpath);
            var versionElement = infoElement.XPathSelectElement(Constants.Packaging.PackageVersionXpath);
            var urlElement = infoElement.XPathSelectElement(Constants.Packaging.PackageUrlXpath);
            var licenseElement = infoElement.XPathSelectElement(Constants.Packaging.PackageLicenseXpath);
            var authorNameElement = infoElement.XPathSelectElement(Constants.Packaging.AuthorNameXpath);
            var authorUrlElement = infoElement.XPathSelectElement(Constants.Packaging.AuthorWebsiteXpath);
            var readmeElement = infoElement.XPathSelectElement(Constants.Packaging.ReadmeXpath);

            XElement controlElement = xRootElement.Element(Constants.Packaging.ControlNodeName);

            return new MetaData
                   {
                       Name = StringValue(nameElement),
                       Version = StringValue(versionElement),
                       Url = StringValue(urlElement),
                       License = StringValue(licenseElement),
                       LicenseUrl = StringAttribute(licenseElement, Constants.Packaging.PackageLicenseXpathUrlAttribute),
                       AuthorName = StringValue(authorNameElement),
                       AuthorUrl = StringValue(authorUrlElement),
                       Readme = StringValue(readmeElement),
                       Control = StringValue(controlElement),
                       ReqMajor = IntValue(majorElement),
                       ReqMinor = IntValue(minorElement),
                       ReqPatch = IntValue(patchElement)
                   };
        }

        private static string StringValue(XElement xElement, string defaultValue = "")
        {
            return xElement == null ? defaultValue : xElement.Value;
        }

        private static string StringAttribute(XElement xElement, string attribute, string defaultValue = "")
        {
            return xElement == null
                        ? defaultValue
                        : xElement.HasAttributes ? xElement.AttributeValue<string>(attribute) : defaultValue;
        }

        private static int IntValue(XElement xElement, int defaultValue = 0)
        {
            int val;
            return xElement == null ? defaultValue : int.TryParse(xElement.Value, out val) ? val : defaultValue;
        }
        
        private static string UpdatePathPlaceholders(string path)
        {
            if (path.Contains("[$"))
            {
                //this is experimental and undocumented...
                path = path.Replace("[$UMBRACO]", SystemDirectories.Umbraco);
                path = path.Replace("[$UMBRACOCLIENT]", SystemDirectories.UmbracoClient);
                path = path.Replace("[$CONFIG]", SystemDirectories.Config);
                path = path.Replace("[$DATA]", SystemDirectories.Data);
            }
            return path;
        }
    }
}