using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using NuGet.Versioning;
using Velopack.Logging;
using Velopack.NuGet;
using Velopack.Util;
namespace Velopack.Locators
{
    /// 
    /// An implementation for Windows which uses the default paths.
    /// 
    [SupportedOSPlatform("windows")]
    public class WindowsVelopackLocator : VelopackLocator
    {
        /// 
        public override string? AppId { get; }
        /// 
        public override string? RootAppDir { get; }
        /// 
        public override string? UpdateExePath { get; }
        /// 
        public override string? AppContentDir { get; }
        /// 
        public override SemanticVersion? CurrentlyInstalledVersion { get; }
        private readonly Lazy _packagesDir;
        /// 
        public override string? PackagesDir => _packagesDir.Value;
        /// 
        public override bool IsPortable => RootAppDir != null && File.Exists(Path.Combine(RootAppDir, ".portable"));
        /// 
        public override IProcessImpl Process { get; }
        /// 
        public override string? Channel { get; }
        /// 
        public WindowsVelopackLocator(IProcessImpl? processImpl, IVelopackLogger? customLog)
        {
            if (!VelopackRuntimeInfo.IsWindows)
                throw new NotSupportedException($"Cannot instantiate {nameof(WindowsVelopackLocator)} on a non-Windows system.");
            _packagesDir = new(GetPackagesDir);
            CombinedLogger = new CombinedVelopackLogger(customLog);
            Process = processImpl ??= new DefaultProcessImpl(CombinedLogger);
            var ourPath = processImpl.GetCurrentProcessPath();
            var currentProcessId = processImpl.GetCurrentProcessId();
            using var initLog = new CachedVelopackLogger(CombinedLogger);
            initLog.Info($"Initializing {nameof(WindowsVelopackLocator)}");
            // We try various approaches here. Firstly, if Update.exe is in the parent directory,
            // we use that. If it's not present, we search for a parent "current" or "app-{ver}" directory,
            // which could designate that this executable is running in a nested sub-directory.
            // There is some legacy code here, because it's possible that we're running in an "app-{ver}" 
            // directory which is NOT containing a sq.version, in which case we need to infer a lot of info.
            string myDirPath = Path.GetDirectoryName(ourPath)!;
            var myDirName = Path.GetFileName(myDirPath);
            var possibleUpdateExe = Path.GetFullPath(Path.Combine(myDirPath, "..", "Update.exe"));
            var ixCurrent = ourPath.LastIndexOf("/current/", StringComparison.InvariantCultureIgnoreCase);
            if (File.Exists(possibleUpdateExe)) {
                // we're running in a directory with an Update.exe in the parent directory
                var manifestFile = Path.Combine(myDirPath, CoreUtil.SpecVersionFileName);
                var rootDir = Path.GetDirectoryName(possibleUpdateExe)!;
                if (PackageManifest.TryParseFromFile(manifestFile, out var manifest)) {
                    // ideal, the info we need is in a manifest file.
                    initLog.Info($"{nameof(WindowsVelopackLocator)}: Update.exe in parent dir, Located valid manifest file at: " + manifestFile);
                    AppId = manifest.Id;
                    CurrentlyInstalledVersion = manifest.Version;
                    RootAppDir = rootDir;
                    UpdateExePath = possibleUpdateExe;
                    AppContentDir = myDirPath;
                    Channel = manifest.Channel;
                } else if (PathUtil.PathPartStartsWith(myDirName, "app-") && NuGetVersion.TryParse(myDirName.Substring(4), out var version)) {
                    // this is a legacy case, where we're running in an 'root/app-*/' directory, and there is no manifest.
                    initLog.Warn(
                        "Update.exe in parent dir, Legacy app-* directory detected, sq.version not found. Using directory name for AppId and Version.");
                    AppId = Path.GetFileName(Path.GetDirectoryName(possibleUpdateExe));
                    CurrentlyInstalledVersion = version;
                    RootAppDir = rootDir;
                    UpdateExePath = possibleUpdateExe;
                    AppContentDir = myDirPath;
                } else {
                    initLog.Error("Update.exe in parent dir, but unable to locate a valid manifest file at: " + manifestFile);
                }
            } else if (ixCurrent > 0) {
                // this is an attempt to handle the case where we are running in a nested current directory.
                var rootDir = ourPath.Substring(0, ixCurrent);
                var currentDir = Path.Combine(rootDir, "current");
                var manifestFile = Path.Combine(currentDir, CoreUtil.SpecVersionFileName);
                possibleUpdateExe = Path.GetFullPath(Path.Combine(rootDir, "Update.exe"));
                // we only support parsing a manifest when we're in a nested current directory. no legacy fallback.
                if (File.Exists(possibleUpdateExe) && PackageManifest.TryParseFromFile(manifestFile, out var manifest)) {
                    initLog.Warn("Running in deeply nested directory. This is not an advised use-case.");
                    initLog.Info("Located valid manifest file at: " + manifestFile);
                    RootAppDir = Path.GetDirectoryName(possibleUpdateExe);
                    UpdateExePath = possibleUpdateExe;
                    AppId = manifest.Id;
                    CurrentlyInstalledVersion = manifest.Version;
                    AppContentDir = currentDir;
                    Channel = manifest.Channel;
                }
            }
            if (UpdateExePath != null
                && Path.GetDirectoryName(UpdateExePath) is { } updateExeDirectory
                && !PathUtil.IsDirectoryWritable(updateExeDirectory)) {
                var tempTargetUpdateExe = Path.Combine(TempAppRootDirectory, "Update.exe");
                if (File.Exists(UpdateExePath) && !File.Exists(tempTargetUpdateExe)) {
                    initLog.Warn("Application directory is not writable. Copying Update.exe to temp location: " + tempTargetUpdateExe);
                    Debugger.Launch();
                    Directory.CreateDirectory(TempAppRootDirectory);
                    File.Copy(UpdateExePath, tempTargetUpdateExe);
                }
                UpdateExePath = tempTargetUpdateExe;
            }
            //bool fileLogCreated = false;
            Exception? fileLogException = null;
            if (!String.IsNullOrEmpty(AppId) && !String.IsNullOrEmpty(RootAppDir)) {
                try {
                    var logFilePath = Path.Combine(RootAppDir, DefaultLoggingFileName);
                    var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
                    CombinedLogger.Add(fileLog);
                    //fileLogCreated = true;
                } catch (Exception ex) {
                    fileLogException = ex;
                }
            }
            // if the RootAppDir was unwritable, or we don't know the app id, we could try to write to the temp folder instead.
            Exception? tempFileLogException = null;
            if (fileLogException is not null) {
                try {
                    var logFileName = String.IsNullOrEmpty(AppId) ? DefaultLoggingFileName : $"velopack_{AppId}.log";
                    var logFilePath = Path.Combine(Path.GetTempPath(), logFileName);
                    var fileLog = new FileVelopackLogger(logFilePath, currentProcessId);
                    CombinedLogger.Add(fileLog);
                } catch (Exception ex) {
                    tempFileLogException = ex;
                }
            }
            if (tempFileLogException is not null) {
                //NB: fileLogException is not null here
                initLog.Error("Unable to create file logger: " + new AggregateException(fileLogException!, tempFileLogException));
            } else if (fileLogException is not null) {
                initLog.Info("Unable to create file logger; using temp directory for log instead");
                initLog.Trace($"File logger exception: {fileLogException}");
            }
            if (AppId == null) {
                initLog.Warn(
                    $"Failed to initialize {nameof(WindowsVelopackLocator)}. This could be because the program is not installed or packaged properly.");
            } else {
                initLog.Info($"Initialized {nameof(WindowsVelopackLocator)} for {AppId} v{CurrentlyInstalledVersion}");
            }
        }
        private string? GetPackagesDir()
        {
            const string PackagesDirName = "packages";
            string? writableRootDir = PossibleDirectories()
                .FirstOrDefault(IsWritable);
            if (writableRootDir == null) {
                Log.Warn("Unable to find a writable root directory for package.");
                return null;
            }
            Log.Trace("Using writable root directory: " + writableRootDir);
            return CreateSubDirIfDoesNotExist(writableRootDir, PackagesDirName);
            static bool IsWritable(string? directoryPath)
            {
                if (directoryPath == null) return false;
                try {
                    if (!Directory.Exists(directoryPath)) {
                        Directory.CreateDirectory(directoryPath);
                    }
                    return PathUtil.IsDirectoryWritable(directoryPath);
                } catch {
                    return false;
                }
            }
            IEnumerable PossibleDirectories()
            {
                yield return RootAppDir;
                yield return TempAppRootDirectory;
            }
        }
        private string TempAppRootDirectory => Path.Combine(Path.GetTempPath(), "velopack_" + AppId);
    }
}