#include "pch.h"

#include <vcpkg/commands.h>
#include <vcpkg/dependencies.h>
#include <vcpkg/globalstate.h>
#include <vcpkg/help.h>
#include <vcpkg/input.h>
#include <vcpkg/install.h>
#include <vcpkg/statusparagraphs.h>
#include <vcpkg/update.h>
#include <vcpkg/vcpkglib.h>

#include <vcpkg/base/util.h>

namespace vcpkg::Commands::Upgrade
{
    using Install::KeepGoing;
    using Install::to_keep_going;

    static constexpr StringLiteral OPTION_NO_DRY_RUN = "--no-dry-run";
    static constexpr StringLiteral OPTION_KEEP_GOING = "--keep-going";

    static constexpr std::array<CommandSwitch, 2> INSTALL_SWITCHES = {{
        {OPTION_NO_DRY_RUN, "Actually upgrade"},
        {OPTION_KEEP_GOING, "Continue installing packages on failure"},
    }};

    const CommandStructure COMMAND_STRUCTURE = {
        Help::create_example_string("upgrade --no-dry-run"),
        0,
        SIZE_MAX,
        {INSTALL_SWITCHES, {}},
        nullptr,
    };

    void perform_and_exit(const VcpkgCmdArguments& args, const VcpkgPaths& paths, const Triplet& default_triplet)
    {
        const ParsedArguments options = args.parse_arguments(COMMAND_STRUCTURE);

        const bool no_dry_run = Util::Sets::contains(options.switches, OPTION_NO_DRY_RUN);
        const KeepGoing keep_going = to_keep_going(Util::Sets::contains(options.switches, OPTION_KEEP_GOING));

        StatusParagraphs status_db = database_load_check(paths);

        Dependencies::PathsPortFileProvider provider(paths);
        Dependencies::PackageGraph graph(provider, status_db);

        // input sanitization
        const std::vector<PackageSpec> specs = Util::fmap(args.command_arguments, [&](auto&& arg) {
            return Input::check_and_get_package_spec(arg, default_triplet, COMMAND_STRUCTURE.example_text);
        });

        for (auto&& spec : specs)
        {
            Input::check_triplet(spec.triplet(), paths);
        }

        if (specs.empty())
        {
            // If no packages specified, upgrade all outdated packages.
            auto outdated_packages = Update::find_outdated_packages(provider, status_db);

            if (outdated_packages.empty())
            {
                System::println("All installed packages are up-to-date with the local portfiles.");
                Checks::exit_success(VCPKG_LINE_INFO);
            }

            for (auto&& outdated_package : outdated_packages)
                graph.upgrade(outdated_package.spec);
        }
        else
        {
            std::vector<PackageSpec> not_installed;
            std::vector<PackageSpec> no_portfile;
            std::vector<PackageSpec> to_upgrade;
            std::vector<PackageSpec> up_to_date;

            for (auto&& spec : specs)
            {
                auto it = status_db.find_installed(spec);
                if (it == status_db.end())
                {
                    not_installed.push_back(spec);
                }

                auto maybe_scf = provider.get_control_file(spec.name());
                if (auto p_scf = maybe_scf.get())
                {
                    if (it != status_db.end())
                    {
                        if (p_scf->core_paragraph->version != (*it)->package.version)
                        {
                            to_upgrade.push_back(spec);
                        }
                        else
                        {
                            up_to_date.push_back(spec);
                        }
                    }
                }
                else
                {
                    no_portfile.push_back(spec);
                }
            }

            Util::sort(not_installed);
            Util::sort(no_portfile);
            Util::sort(up_to_date);
            Util::sort(to_upgrade);

            if (!up_to_date.empty())
            {
                System::println(System::Color::success, "The following packages are up-to-date:");
                System::println(Strings::join(
                    "", up_to_date, [](const PackageSpec& spec) { return "    " + spec.to_string() + "\n"; }));
            }

            if (!not_installed.empty())
            {
                System::println(System::Color::error, "The following packages are not installed:");
                System::println(Strings::join(
                    "", not_installed, [](const PackageSpec& spec) { return "    " + spec.to_string() + "\n"; }));
            }

            if (!no_portfile.empty())
            {
                System::println(System::Color::error, "The following packages do not have a valid portfile:");
                System::println(Strings::join(
                    "", no_portfile, [](const PackageSpec& spec) { return "    " + spec.to_string() + "\n"; }));
            }

            Checks::check_exit(VCPKG_LINE_INFO, not_installed.empty() && no_portfile.empty());

            if (to_upgrade.empty()) Checks::exit_success(VCPKG_LINE_INFO);

            for (auto&& spec : to_upgrade)
                graph.upgrade(spec);
        }

        auto plan = graph.serialize();

        Checks::check_exit(VCPKG_LINE_INFO, !plan.empty());

        const Build::BuildPackageOptions install_plan_options = {
            Build::UseHeadVersion::NO,
            Build::AllowDownloads::YES,
            Build::CleanBuildtrees::NO,
            Build::CleanPackages::NO,
            Build::DownloadTool::BUILT_IN,
            GlobalState::g_binary_caching ? Build::BinaryCaching::YES : Build::BinaryCaching::NO,
            Build::FailOnTombstone::NO,
        };

        // Set build settings for all install actions
        for (auto&& action : plan)
        {
            if (auto p_install = action.install_action.get())
            {
                p_install->build_options = install_plan_options;
            }
        }

        Dependencies::print_plan(plan, true);

        if (!no_dry_run)
        {
            System::println(System::Color::warning,
                            "If you are sure you want to rebuild the above packages, run this command with the "
                            "--no-dry-run option.");
            Checks::exit_fail(VCPKG_LINE_INFO);
        }

        const Install::InstallSummary summary = Install::perform(plan, keep_going, paths, status_db);

        System::println("\nTotal elapsed time: %s\n", summary.total_elapsed_time);

        if (keep_going == KeepGoing::YES)
        {
            summary.print();
        }

        Checks::exit_success(VCPKG_LINE_INFO);
    }
}
