package thb.jeanluc.adventure;

import lombok.extern.slf4j.Slf4j;
import thb.jeanluc.adventure.command.CommandParser;
import thb.jeanluc.adventure.command.CommandRegistry;
import thb.jeanluc.adventure.command.impl.DropCommand;
import thb.jeanluc.adventure.command.impl.ExamineCommand;
import thb.jeanluc.adventure.command.impl.GiveCommand;
import thb.jeanluc.adventure.command.impl.GoCommand;
import thb.jeanluc.adventure.command.impl.HelpCommand;
import thb.jeanluc.adventure.command.impl.InventoryCommand;
import thb.jeanluc.adventure.command.impl.LookCommand;
import thb.jeanluc.adventure.command.impl.MapCommand;
import thb.jeanluc.adventure.command.impl.MenuCommand;
import thb.jeanluc.adventure.command.impl.QuestsCommand;
import thb.jeanluc.adventure.command.impl.ReadCommand;
import thb.jeanluc.adventure.command.impl.SaveCommand;
import thb.jeanluc.adventure.command.impl.TakeCommand;
import thb.jeanluc.adventure.command.impl.TalkCommand;
import thb.jeanluc.adventure.command.impl.UseCommand;
import thb.jeanluc.adventure.game.Game;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.game.GameSession;
import thb.jeanluc.adventure.game.TutorialGuide;
import thb.jeanluc.adventure.io.ConsoleIO;
import thb.jeanluc.adventure.io.GameIO;
import thb.jeanluc.adventure.io.text.Banner;
import thb.jeanluc.adventure.loader.TutorialLoader;
import thb.jeanluc.adventure.loader.WorldLoader;
import thb.jeanluc.adventure.menu.MainMenu;
import thb.jeanluc.adventure.menu.SettingsMenu;
import thb.jeanluc.adventure.model.Tutorial;
import thb.jeanluc.adventure.save.SaveException;
import thb.jeanluc.adventure.save.SaveService;
import thb.jeanluc.adventure.save.SaveSlotInfo;
import thb.jeanluc.adventure.save.Settings;
import thb.jeanluc.adventure.save.SettingsStore;

import java.util.List;

/**
 * Console entry point. Loads settings, then runs the main-menu shell loop:
 * New Game / Load / Settings / Quit. Each game runs the shared {@link Game}
 * loop and returns here when the player saves-and-exits, reaches an ending,
 * or input ends. Shared by console and GUI via {@link #run(GameIO)}.
 */
@Slf4j
public final class App {

    private App() {
    }

    /** Standard JVM entry point. @param args ignored */
    public static void main(String[] args) {
        GameIO io = new ConsoleIO();
        try {
            run(io);
        } catch (RuntimeException e) {
            log.error("Fatal error during game startup", e);
            io.write("Fatal error: " + e.getMessage());
            System.exit(1);
        }
    }

    /** Runs the menu shell on the given IO until the player quits. */
    public static void run(GameIO io) {
        run(io, new SaveService(), new SettingsStore());
    }

    /** Runs the menu shell with injectable persistence (for testing). */
    static void run(GameIO io, SaveService saves, SettingsStore settingsStore) {
        Settings settings = settingsStore.load();
        SettingsMenu.apply(settings, io);

        boolean running = true;
        while (running) {
            switch (MainMenu.show(io)) {
                case NEW_GAME -> play(io, saves, newSession(io), true);
                case LOAD -> {
                    SaveSlotInfo slot = MainMenu.pickSlot(io, saves);
                    if (slot != null) {
                        try {
                            play(io, saves, saves.load(slot.slug()), false);
                        } catch (SaveException e) {
                            io.write("Could not load: " + e.getUserMessage());
                        }
                    }
                }
                case SETTINGS -> settings = SettingsMenu.show(io, settings, settingsStore);
                case QUIT -> running = false;
            }
        }
        io.write("Farewell.");
        io.shutdown(); // GUI: close the window and exit; console: no-op
    }

    /** Loads a fresh world and binds it to a newly named session. */
    private static GameSession newSession(GameIO io) {
        WorldLoader.LoadResult loaded = new WorldLoader().load();
        String name = MainMenu.promptName(io, "Manor");
        return new GameSession(loaded.world(), loaded.player(), name);
    }

    /** Builds the registry, runs one game to completion, autosaving to its slot. */
    private static void play(GameIO io, SaveService saves, GameSession session, boolean withTutorial) {
        GameContext ctx = new GameContext(session, io);
        ctx.setSaveCallback(() -> {
            try {
                saves.save(session);
            } catch (SaveException e) {
                log.warn("Autosave failed", e);
            }
        });

        CommandRegistry registry = new CommandRegistry();
        registry.register(new GoCommand(), "go", "move", "walk");
        registry.register(new LookCommand(), "look", "l");
        registry.register(new InventoryCommand(), "inventory", "inv", "i");
        registry.register(new TakeCommand(), "take", "pick", "get");
        registry.register(new DropCommand(), "drop", "put");
        registry.register(new UseCommand(), "use");
        registry.register(new ReadCommand(), "read");
        registry.register(new ExamineCommand(), "examine", "x", "inspect");
        registry.register(new MapCommand(), "map", "m");
        registry.register(new QuestsCommand(), "quests", "log", "journal");
        registry.register(new TalkCommand(), "talk", "speak");
        registry.register(new GiveCommand(), "give");
        registry.register(new SaveCommand(saves), "save");

        Game game = new Game(ctx, registry, new CommandParser());

        MenuCommand menu = new MenuCommand(saves);
        menu.bind(game);
        registry.register(menu, "menu", "quit", "exit");
        registry.register(new HelpCommand(registry), "help", "?");

        if (withTutorial) {
            Tutorial tutorial = new TutorialLoader().load();
            if (!tutorial.isEmpty()) {
                game.setTutorialGuide(new TutorialGuide(tutorial, registry));
            }
        }

        io.print(Banner.welcome(session.getWorld().getTitle()));
        io.write(session.getWorld().getWelcomeMessage());
        new LookCommand().execute(ctx, List.of());

        game.run();
        io.setMusic(null); // fade music out when returning to the menu
    }
}
