package thb.jeanluc.adventure.game;

import thb.jeanluc.adventure.command.Command;
import thb.jeanluc.adventure.command.CommandRegistry;
import thb.jeanluc.adventure.command.ParsedCommand;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.Tutorial;
import thb.jeanluc.adventure.model.TutorialStep;

import java.util.Optional;

/**
 * Drives the interactive start-of-game walkthrough, layered onto the real game
 * loop. Prints an instruction, then on each executed command decides whether the
 * current step is satisfied (confirm + advance) or not (hint). Movement steps
 * require an actual room change. Alias-aware via the {@link CommandRegistry}.
 */
public final class TutorialGuide {

    private final Tutorial tutorial;
    private final CommandRegistry registry;
    private int index;
    private boolean active;
    private boolean begun;
    private Room roomAtStepStart;

    public TutorialGuide(Tutorial tutorial, CommandRegistry registry) {
        this.tutorial = tutorial;
        this.registry = registry;
        this.active = !tutorial.isEmpty();
    }

    /** @return true while the walkthrough is still running. */
    public boolean isActive() {
        return active;
    }

    /** Prints the intro and the first instruction (call once at loop start). */
    public void begin(GameContext ctx) {
        if (!active || begun) {
            return;
        }
        begun = true;
        if (tutorial.intro() != null && !tutorial.intro().isBlank()) {
            ctx.getIo().write(tutorial.intro());
        }
        enterStep(ctx);
    }

    /** Evaluates the current step against the command that was just executed. */
    public void onCommand(GameContext ctx, ParsedCommand parsed) {
        if (!active) {
            return;
        }
        TutorialStep step = tutorial.steps().get(index);
        if (satisfies(step, ctx, parsed)) {
            ctx.getIo().write(step.confirm());
            index++;
            if (index >= tutorial.steps().size()) {
                if (tutorial.closingTips() != null && !tutorial.closingTips().isBlank()) {
                    ctx.getIo().write(tutorial.closingTips());
                }
                active = false;
            } else {
                enterStep(ctx);
            }
        } else {
            ctx.getIo().write(step.hint());
        }
    }

    /** Ends the tutorial early (the 'skip' command). */
    public void skip(GameContext ctx) {
        if (active) {
            ctx.getIo().write("Tutorial skipped.");
            active = false;
        }
    }

    private void enterStep(GameContext ctx) {
        roomAtStepStart = ctx.getPlayer().getCurrentRoom();
        ctx.getIo().write(tutorial.steps().get(index).instruction());
    }

    private boolean satisfies(TutorialStep step, GameContext ctx, ParsedCommand parsed) {
        String expect = step.expect();
        if ("go_direction".equals(expect)) {
            return isGo(parsed) && firstIsDirection(parsed) && moved(ctx);
        }
        if ("go_to_room".equals(expect)) {
            return isGo(parsed) && !firstIsDirection(parsed) && moved(ctx);
        }
        return sameCommand(parsed.verb(), expect) && parsed.args().size() >= step.minArgs();
    }

    private boolean isGo(ParsedCommand p) {
        return sameCommand(p.verb(), "go") && !p.args().isEmpty();
    }

    private boolean firstIsDirection(ParsedCommand p) {
        if (p.args().isEmpty()) {
            return false;
        }
        try {
            Direction.fromString(p.args().get(0));
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    private boolean moved(GameContext ctx) {
        return roomAtStepStart != null
                && ctx.getPlayer().getCurrentRoom() != roomAtStepStart;
    }

    private boolean sameCommand(String a, String b) {
        Optional<Command> ca = registry.find(a);
        Optional<Command> cb = registry.find(b);
        return ca.isPresent() && cb.isPresent() && ca.get() == cb.get();
    }
}
