namespace Jint.Runtime.Modules;

public class DefaultModuleLoader : ModuleLoader
{
    private readonly Uri _basePath;
    private readonly bool _restrictToBasePath;

    public DefaultModuleLoader(string basePath, bool restrictToBasePath = true)
    {
        if (string.IsNullOrWhiteSpace(basePath))
        {
            Throw.ArgumentException("Value cannot be null or whitespace.", nameof(basePath));
        }

        _restrictToBasePath = restrictToBasePath;

        if (!Uri.TryCreate(basePath, UriKind.Absolute, out var temp))
        {
            if (!Path.IsPathRooted(basePath))
            {
                Throw.ArgumentException("Path must be rooted", nameof(basePath));
            }

            basePath = Path.GetFullPath(basePath);
            _basePath = new Uri(basePath, UriKind.Absolute);
        }
        else
        {
            _basePath = temp;
        }

        if (_basePath.AbsolutePath[^1] != '/')
        {
            var uriBuilder = new UriBuilder(_basePath);
            uriBuilder.Path += '/';
            _basePath = uriBuilder.Uri;
        }
    }

    public override ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
    {
        var specifier = moduleRequest.Specifier;
        if (string.IsNullOrEmpty(specifier))
        {
            Throw.ModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
            return default;
        }

        // Specifications from ESM_RESOLVE Algorithm: https://nodejs.org/api/esm.html#resolution-algorithm

        Uri resolved;
        if (Uri.TryCreate(specifier, UriKind.Absolute, out var uri))
        {
            resolved = uri;
        }
        else if (IsRelative(specifier))
        {
            var baseUri = BuildBaseUri(referencingModuleLocation);
            resolved = new Uri(baseUri, specifier);
        }
        else if (specifier[0] == '#')
        {
            Throw.NotSupportedException($"PACKAGE_IMPORTS_RESOLVE is not supported: '{specifier}'");
            return default;
        }
        else
        {
            return new ResolvedSpecifier(
                moduleRequest,
                specifier,
                Uri: null,
                SpecifierType.Bare
            );
        }

        if (resolved.IsFile)
        {
            if (resolved.UserEscaped)
            {
                Throw.ModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
                return default;
            }

            if (!Path.HasExtension(resolved.LocalPath))
            {
                Throw.ModuleResolutionException("Unsupported Directory Import", specifier, referencingModuleLocation);
                return default;
            }
        }

        if (_restrictToBasePath && !_basePath.IsBaseOf(resolved))
        {
            Throw.ModuleResolutionException($"Unauthorized Module Path", specifier, referencingModuleLocation);
            return default;
        }

        return new ResolvedSpecifier(
            moduleRequest,
            resolved.AbsoluteUri,
            resolved,
            SpecifierType.RelativeOrAbsolute
        );
    }

    private Uri BuildBaseUri(string? referencingModuleLocation)
    {
        if (referencingModuleLocation is not null)
        {
            /*
              "referencingModuleLocation" might be relative or an invalid URI when a module imports other
               modules and the importing module is called directly from .NET code.
               e.g. "engine.Modules.Import("my-module")" and "my-module" imports other modules.

               Path traversal prevention is not a concern here because it is checked later
               (if _restrictToBasePath is set to true).
            */
            if (Uri.TryCreate(referencingModuleLocation, UriKind.Absolute, out var referencingLocation) ||
                Uri.TryCreate(_basePath, referencingModuleLocation, out referencingLocation))
                return referencingLocation;
        }
        return _basePath;
    }

    protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
    {
        var specifier = resolved.ModuleRequest.Specifier;
        if (resolved.Type != SpecifierType.RelativeOrAbsolute)
        {
            Throw.NotSupportedException($"The default module loader can only resolve files. You can define modules directly to allow imports using {nameof(Engine)}.{nameof(Engine.Modules.Add)}(). Attempted to resolve: '{specifier}'.");
        }

        if (resolved.Uri == null)
        {
            Throw.InvalidOperationException($"Module '{specifier}' of type '{resolved.Type}' has no resolved URI.");
        }

        var fileName = Uri.UnescapeDataString(resolved.Uri.AbsolutePath);
        if (!File.Exists(fileName))
        {
            Throw.ModuleResolutionException("Module Not Found", specifier, parent: null, fileName);
        }

        return File.ReadAllText(fileName);
    }

    private static bool IsRelative(string specifier)
    {
        return specifier.StartsWith('.') || specifier.StartsWith('/');
    }
}
