load("@rules_dotnet//dotnet/private:common.bzl", "is_debug")
load("@rules_dotnet//dotnet/private:providers.bzl", "DotnetAssemblyRuntimeInfo")
load(":dotnet_utils.bzl", "dotnet_preamble")
load(":providers.bzl", "NugetPackageInfo")

_CSPROJ_TEMPLATE = """\
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <AssemblyName>{id}</AssemblyName>
    </PropertyGroup>
</Project>
"""

def _guess_dotnet_version(assembly_info):
    if len(assembly_info.libs) == 0:
        fail("Cannot guess .Net version without an output dll: ", assembly_info.name)

    # The dirname will be something like
    # `bazel-out/darwin_arm64-fastbuild-ST-5c013bc87029/bin/dotnet/src/webdriver/bazelout/net5.0`
    # The last segment of the path is the target framework.
    return assembly_info.libs[0].dirname.split("/")[::-1][0]

def nuget_pack_impl(ctx):
    nuspec = ctx.actions.declare_file("%s-generated.nuspec" % ctx.label.name)
    ctx.actions.expand_template(
        template = ctx.file.nuspec_template,
        output = nuspec,
        substitutions = {
            "$packageid$": ctx.attr.id,
            "$version$": ctx.attr.version,
        },
    )

    build_flavor = "Debug" if is_debug(ctx) else "Release"

    # Collect files and their target paths within the package layout
    layout = {}
    for (lib, name) in ctx.attr.libs.items():
        assembly_info = lib[DotnetAssemblyRuntimeInfo]
        tfm = _guess_dotnet_version(assembly_info)
        for dll in assembly_info.libs:
            layout[dll] = "lib/%s/%s.dll" % (tfm, name)
        for pdb in assembly_info.pdbs:
            layout[pdb] = "lib/%s/%s.pdb" % (tfm, name)
        for doc in assembly_info.xml_docs:
            layout[doc] = "lib/%s/%s.xml" % (tfm, name)

    for (file, name) in ctx.attr.files.items():
        layout[file.files.to_list()[0]] = name

    csproj_file = ctx.actions.declare_file("%s-generated.csproj" % ctx.label.name)
    ctx.actions.write(csproj_file, _CSPROJ_TEMPLATE.format(id = ctx.attr.id))

    # Prepare transitive NuGet package cache
    transitive_libs = depset(transitive = [l[DotnetAssemblyRuntimeInfo].deps for l in ctx.attr.libs]).to_list()
    package_files = depset([lib.nuget_info.nupkg for lib in transitive_libs if lib.nuget_info]).to_list()

    packages = ctx.actions.declare_directory("%s-nuget-packages" % ctx.label.name)
    packages_cmd = "mkdir -p '%s'" % packages.path
    if package_files:
        packages_cmd += " && cp " + " ".join(["'%s'" % f.path for f in package_files]) + " '%s'" % packages.path

    ctx.actions.run_shell(
        outputs = [packages],
        inputs = package_files,
        command = packages_cmd,
        mnemonic = "LayoutNugetPackages",
    )

    # Create nupkg via dotnet pack
    toolchain = ctx.toolchains["@rules_dotnet//dotnet:toolchain_type"]
    nupkg_stem = "%s.%s" % (ctx.attr.id, ctx.attr.version)
    dotnet = toolchain.runtime.files_to_run.executable
    pkg = ctx.actions.declare_file("%s.nupkg" % nupkg_stem)
    symbols_pkg = ctx.actions.declare_file("%s.snupkg" % nupkg_stem)

    working_dir = ctx.label.name + "-working-dir"

    # Copy files directly into the working directory layout (no intermediate zip)
    copy_cmds = []
    for (file, rel_path) in layout.items():
        dest = working_dir + "/" + rel_path
        copy_cmds.append("mkdir -p \"$(dirname '{dest}')\" && cp '{src}' '{dest}'".format(
            dest = dest,
            src = file.path,
        ))

    cmd_parts = [
        "rm -rf '%s'" % working_dir,
        "mkdir -p '%s'" % working_dir,
    ] + copy_cmds + [
        "cp '{src}' '{dir}/project.nuspec'".format(src = nuspec.path, dir = working_dir),
        "cp '{src}' '{dir}/project.csproj'".format(src = csproj_file.path, dir = working_dir),
        "cd '%s'" % working_dir,
        "echo '<configuration><packageSources><clear /><add key=\"local\" value=\"%CWD%/{packages}\" /></packageSources></configuration>' >nuget.config".format(
            packages = packages.path,
        ),
        "$DOTNET restore --no-dependencies",
        " ".join([
            "$DOTNET pack --no-build --include-symbols",
            "-p:NuspecFile=project.nuspec",
            "-p:SymbolPackageFormat=snupkg",
            "-p:NoWarn=NU5048",  # suppress 'iconUrl is deprecated, use icon' warning since nuspec uses both
            "-p:Configuration=" + build_flavor,
            "-p:PackageId=" + ctx.attr.id,
            "-p:Version=" + ctx.attr.version,
            "-p:PackageVersion=" + ctx.attr.version,
            "-p:NuspecProperties=\"version=" + ctx.attr.version + "\"",
        ]),
        "cp 'bin/{flavor}/{stem}.nupkg' '../{pkg}'".format(
            flavor = build_flavor,
            stem = nupkg_stem,
            pkg = pkg.path,
        ),
        "cp 'bin/{flavor}/{stem}.snupkg' '../{symbols}'".format(
            flavor = build_flavor,
            stem = nupkg_stem,
            symbols = symbols_pkg.path,
        ),
    ]

    cmd = dotnet_preamble(toolchain) + " && ".join(cmd_parts)

    ctx.actions.run_shell(
        outputs = [pkg, symbols_pkg],
        inputs = list(layout.keys()) + [nuspec, csproj_file, dotnet, packages],
        tools = [dotnet] + toolchain.default.files.to_list() + toolchain.runtime.default_runfiles.files.to_list() + toolchain.runtime.data_runfiles.files.to_list(),
        command = cmd,
        mnemonic = "CreateNupkg",
    )

    return [
        DefaultInfo(
            files = depset([pkg, symbols_pkg]),
            runfiles = ctx.runfiles(files = [pkg, symbols_pkg]),
        ),
        NugetPackageInfo(
            package = pkg,
            symbols = symbols_pkg,
        ),
    ]

nuget_pack = rule(
    nuget_pack_impl,
    attrs = {
        "id": attr.string(
            doc = "Nuget ID of the package",
            mandatory = True,
        ),
        "version": attr.string(
            mandatory = True,
        ),
        "libs": attr.label_keyed_string_dict(
            doc = "The .Net libraries that are being published",
            providers = [DotnetAssemblyRuntimeInfo],
        ),
        "files": attr.label_keyed_string_dict(
            doc = "Mapping of files to paths within the nuget package",
            allow_empty = True,
            allow_files = True,
        ),
        "nuspec_template": attr.label(
            mandatory = True,
            allow_single_file = True,
        ),
    },
    toolchains = ["@rules_dotnet//dotnet:toolchain_type"],
)
