import Hummingbird
import Logging
import NIOPosix

/// A middleware that proxy requests to content inside `.doccarchive` archive containers located in a hosting app.
///
/// The routing logic this middleware implements are:
/// 1. Send all requests starting with `/documentation/` or `/tutorials/` to the `index.html` file;
/// 2. Send all requests starting with `/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`, `/index/`, `/js/`, or `/videos/` to their respective folders;
/// 3. Send all requests to `favicon.ico`, `favicon.svg`, and `theme-settings.json` to their respective files;
/// 4. Send all requests to `/data/documentation.json` to the file in the `data/documentation/` folder that has the name of the module and ends with the `.json` extension;
/// 5. Redirect requests to `/` and `/documentation` to the`/documentation/` folder;
/// 6. Redirect requests to `/tutorials` to  the`/tutorials/` folder.
struct DocCMiddleware<
    Context: RequestContext,
    AssetProvider: FileProvider
>: RouterMiddleware {
    
    // MARK: Properties
    
    private let assetProvider: AssetProvider
    
    // MARK: Initialisers
    
    /// Initialises this middleware with the local file system provider.
    /// - Parameters:
    ///   - rootFolder: A root folder in the local file system where the *DocC* archive containers are located.
    ///   - threadPool: A thread pool used when loading archives from the file system.
    ///   - logger: A Logger that outputs information about the root folder requests.
    init(
        _ rootFolder: String,
        threadPool: NIOThreadPool = .singleton,
        logger: Logger = .init(label: "DocCMiddleware")
    ) where AssetProvider == LocalFileSystem {
        self.assetProvider = LocalFileSystem(
            rootFolder: rootFolder,
            threadPool: threadPool,
            logger: logger
        )
    }
    
    /// Initialises this middleware with an asset provider, that conforms to the `FileProvider` protocol.
    /// - Parameter assetProvider: An asset provider to use with the middleware.
    init(assetProvider: AssetProvider) {
        self.assetProvider = assetProvider
    }
    
    // MARK: Functions
    
    func handle(
        _ input: Request,
        context: Context,
        next: (Request, Context) async throws -> Response
    ) async throws -> Response {
        guard
            let uriPath = input.uri.path.removingPercentEncoding,
            !uriPath.contains(.previousFolder),
            uriPath.hasPrefix(.forwardSlash)
        else {
            throw HTTPError(.badRequest)
        }

        guard uriPath.starts(with: /^\/archives\/\w+/) else {
            return try await next(input, context)
        }
        
        let pathArchive = PathProvider.archivePath(from: uriPath)
        let nameArchive = PathProvider.archiveName(from: uriPath)
        let uriResource = PathProvider.resourcePath(from: uriPath)

        // rule #5: Redirects URI resources with `/` to `/documentation`.
        if uriResource == .forwardSlash {
            return .redirect(to: String(format: .Format.Path.documentation, uriPath))
        }

        for staticFile in StaticFile.allCases {
            if uriResource.contains(staticFile.path) {
                if staticFile == .documentation {
                    // Rule #4: Redirects URI resources with `/data/documentation.json` to the file in the `data/documentation/`
                    // folder that has the name of the module and ends with the `.json` extension in the *DocC* archive container.
                    return try await serveFile(
                        String(format: .Format.Path.documentationJSON, nameArchive),
                        at: pathArchive,
                        context: context
                    )
                } else {
                    // Rule #3: Redirect URI resources for static files (`favicon.ico`, `favicon.svg`, and `theme-settings.json`)
                    // to their respective files in the *DocC* archive container.
                    return try await serveFile(
                        uriResource,
                        at: pathArchive,
                        context: context
                    )
                }
            }
        }
        
        for assetPrefix in AssetPrefix.allCases {
            if uriResource.contains(assetPrefix.path) {
                // Rule #2: Redirect URI resources for asset files (`/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`,
                // `/index/`, `/js/`, or `/videos/`)to their respective files in the *DocC* archive container.
                return try await serveFile(
                    uriResource,
                    at: pathArchive,
                    context: context
                )
            }
        }
        
        for indexPrefix in IndexPrefix.allCases {
            if uriResource.contains(indexPrefix.path) {
                // Rule #1: Redirect URI resources for `/documentation/` and `/tutorials/` folders to their respective `index.html` file.
                if uriResource.hasSuffix(.forwardSlash) {
                    return try await serveFile(
                        String(format: .Format.Path.index, indexPrefix.path, nameArchive),
                        at: pathArchive,
                        context: context
                    )
                } else {
                    // rule #5: Redirects URI resources with `/documentation` to `/documentation/`.
                    // rule #6: Redirects URI resources with `/tutorials` to `/tutorials/`.
                    return .redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
                }
            }
        }

        throw HTTPError(.notImplemented)
    }
    
}

// MARK: - Helpers

private extension DocCMiddleware {
    
    // MARK: Functions
    
    /// Serves a resource file from a provider as a HTTP response.
    /// - Parameters:
    ///   - path: A relative path to a resource file.
    ///   - folder: A folder accessible to the provider where to find resource files.
    ///   - context: A request context.
    /// - Returns: A HTTP response containing the content of a given resource file inside its body.
    /// - Throws:An error...
    func serveFile(
        _ path: String,
        at folder: String,
        context: Context
    ) async throws -> Response {
        guard let fileIdentifier = assetProvider.getFileIdentifier(folder + path) else {
            throw HTTPError(.notFound)
        }

        let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)

        return .init(
            status: .ok,
            headers: [:],
            body: body
        )
    }
    
}
