package thb.jeanluc.adventure.loader;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import thb.jeanluc.adventure.loader.dto.CombinationDto;
import thb.jeanluc.adventure.loader.dto.EndingDto;
import thb.jeanluc.adventure.loader.dto.GameDto;
import thb.jeanluc.adventure.loader.dto.ItemDto;
import thb.jeanluc.adventure.loader.dto.NpcDto;
import thb.jeanluc.adventure.loader.dto.QuestDto;
import thb.jeanluc.adventure.loader.dto.RoomDto;
import thb.jeanluc.adventure.model.Combination;
import thb.jeanluc.adventure.model.Ending;
import thb.jeanluc.adventure.model.Npc;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Quest;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import thb.jeanluc.adventure.model.item.Item;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Orchestrates the five-phase load (see {@code loading-flow.md}). Reads
 * four YAML files from the classpath, builds DTOs, then domain shells,
 * resolves references, validates, and finally returns the assembled
 * {@link World} plus the freshly placed {@link Player}.
 */
@Slf4j
@RequiredArgsConstructor
public class WorldLoader {

    /** Default classpath base directory for the YAML files. */
    public static final String DEFAULT_BASE = "/world";

    /** Single Jackson mapper, configured once for YAML input. */
    private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());

    /** Classpath base directory (typically {@link #DEFAULT_BASE} or a test override). */
    private final String basePath;

    /**
     * Convenience constructor using {@link #DEFAULT_BASE}.
     */
    public WorldLoader() {
        this(DEFAULT_BASE);
    }

    /**
     * Result of a successful load: world plus the placed player.
     *
     * @param world  the built world
     * @param player the player placed at {@code game.yaml.startRoom}
     */
    public record LoadResult(World world, Player player) {
    }

    /**
     * Loads the world by walking all five phases.
     *
     * @return the world and a placed player
     * @throws WorldLoadException on any phase failure
     */
    public LoadResult load() {
        log.info("Loading world from classpath base '{}'", basePath);

        List<ItemDto> itemDtos = readList(basePath + "/items.yaml", ItemDto.class);
        List<NpcDto> npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class);
        List<RoomDto> roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class);
        List<QuestDto> questDtos = readListOptional(basePath + "/quests.yaml", QuestDto.class);
        List<EndingDto> endingDtos = readListOptional(basePath + "/endings.yaml", EndingDto.class);
        List<CombinationDto> combinationDtos =
                readListOptional(basePath + "/combinations.yaml", CombinationDto.class);
        GameDto gameDto = readSingle(basePath + "/game.yaml", GameDto.class);

        requireUniqueIds("item", itemDtos.stream().map(ItemDto::id).toList());
        requireUniqueIds("npc", npcDtos.stream().map(NpcDto::id).toList());
        requireUniqueIds("room", roomDtos.stream().map(RoomDto::id).toList());
        requireUniqueIds("quest", questDtos.stream().map(QuestDto::id).toList());
        requireUniqueIds("ending", endingDtos.stream().map(EndingDto::id).toList());

        Map<String, Item> items = new HashMap<>();
        for (ItemDto dto : itemDtos) {
            items.put(dto.id(), ItemFactory.fromDto(dto));
        }
        Map<String, Npc> npcs = new HashMap<>();
        for (NpcDto dto : npcDtos) {
            npcs.put(dto.id(), NpcFactory.shellFromDto(dto));
        }
        Map<String, Room> rooms = new HashMap<>();
        for (RoomDto dto : roomDtos) {
            rooms.put(dto.id(), RoomFactory.shellFromDto(dto));
        }
        Map<String, Quest> quests = new HashMap<>();
        for (QuestDto dto : questDtos) {
            quests.put(dto.id(), QuestFactory.fromDto(dto));
        }
        List<Ending> endings = new ArrayList<>();
        for (EndingDto dto : endingDtos) {
            endings.add(EndingFactory.fromDto(dto));
        }

        Map<String, Combination> combinations = new HashMap<>();
        for (CombinationDto dto : combinationDtos) {
            Combination c = CombinationFactory.fromDto(dto, items);
            String key = c.key();
            if (combinations.containsKey(key)) {
                throw new WorldLoadException("Duplicate combination for pair '" + key + "'");
            }
            combinations.put(key, c);
        }

        ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms);
        resolver.resolveRooms(roomDtos);
        resolver.resolveNpcs(npcDtos);

        new WorldValidator(items, npcs, rooms, gameDto).validate();

        Room start = rooms.get(gameDto.startRoom());
        int gold = gameDto.startGold() == null ? 0 : gameDto.startGold();
        Player player = new Player(start, gold);

        World world = new World(rooms, items, npcs,
                gameDto.title(), gameDto.welcomeMessage(), quests, endings, combinations);
        log.info("World '{}' loaded: {} rooms, {} items, {} npcs",
                gameDto.title(), rooms.size(), items.size(), npcs.size());
        return new LoadResult(world, player);
    }

    private <T> List<T> readList(String resource, Class<T> elementType) {
        try (InputStream in = openResource(resource)) {
            CollectionType type = yaml.getTypeFactory().constructCollectionType(List.class, elementType);
            List<T> result = yaml.readValue(in, type);
            return result == null ? List.of() : result;
        } catch (IOException e) {
            throw new WorldLoadException("Failed to parse " + resource, e);
        }
    }

    private <T> List<T> readListOptional(String resource, Class<T> elementType) {
        try (InputStream in = getClass().getResourceAsStream(resource)) {
            if (in == null) {
                return List.of();
            }
            CollectionType type = yaml.getTypeFactory().constructCollectionType(List.class, elementType);
            List<T> result = yaml.readValue(in, type);
            return result == null ? List.of() : result;
        } catch (IOException e) {
            throw new WorldLoadException("Failed to parse " + resource, e);
        }
    }

    private <T> T readSingle(String resource, Class<T> type) {
        try (InputStream in = openResource(resource)) {
            return yaml.readValue(in, type);
        } catch (IOException e) {
            throw new WorldLoadException("Failed to parse " + resource, e);
        }
    }

    private InputStream openResource(String resource) {
        InputStream in = getClass().getResourceAsStream(resource);
        if (in == null) {
            throw new WorldLoadException("Resource not found on classpath: " + resource);
        }
        return in;
    }

    private void requireUniqueIds(String kind, List<String> ids) {
        for (int i = 0; i < ids.size(); i++) {
            for (int j = i + 1; j < ids.size(); j++) {
                if (ids.get(i) != null && ids.get(i).equals(ids.get(j))) {
                    throw new WorldLoadException("Duplicate " + kind + " id: '" + ids.get(i) + "'");
                }
            }
        }
    }
}
