package com.livingworld.core.simulation;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import com.livingworld.core.services.PersistenceService;
import com.livingworld.core.services.TimeService;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleUpdateResult;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.modules.RegionUpdateContext;
import com.livingworld.modules.SimulationModule;
import com.livingworld.regions.Region;

/**
 * Central coordinator for the living world simulation lifecycle.
 * 
 * <p>This class orchestrates tick processing, region updates, module execution,
 * event publishing, and time advancement. It does not implement ecosystem rules —
 * those live in individual {@link com.livingworld.modules.SimulationModule} classes.</p>
 */
public final class SimulationManager {

    public static final int MAX_FORCED_SIMULATION_TICKS = 1000;

    private final SimulationScheduler scheduler;
    private final RegionManager regionManager;
    private final ModuleRegistry moduleRegistry;
    private final LivingWorldEventBus eventBus;
    private final TimeService timeService;
    private final PersistenceService persistenceService;
    private final SimulationProfiler profiler;

    public SimulationManager(SimulationScheduler scheduler,
                             RegionManager regionManager,
                             ModuleRegistry moduleRegistry,
                             LivingWorldEventBus eventBus,
                             TimeService timeService,
                             PersistenceService persistenceService,
                             SimulationProfiler profiler) {
        this.scheduler = requireNonNull(scheduler, "scheduler");
        this.regionManager = requireNonNull(regionManager, "regionManager");
        this.moduleRegistry = requireNonNull(moduleRegistry, "moduleRegistry");
        this.eventBus = requireNonNull(eventBus, "eventBus");
        this.timeService = requireNonNull(timeService, "timeService");
        this.persistenceService = requireNonNull(persistenceService, "persistenceService");
        this.profiler = profiler;
    }

    private static <T> T requireNonNull(T value, String name) {
        if (value == null) {
            throw new IllegalArgumentException(name + " must not be null");
        }
        return value;
    }

    public void onMinecraftServerTick() {
        this.scheduler.onMinecraftTick();

        if (this.scheduler.shouldRunSimulationCycle()) {
            runSimulationCycle();
        }
    }

    /**
     * Runs a single simulation cycle.
     */
    public void runSimulationCycle() {
        long startTimeNanos = System.nanoTime();
        long simulationTick = this.timeService.getSimulationTick();
        if (this.profiler != null) {
            this.profiler.startCycle(simulationTick);
        }

        List<RegionUpdateJob> jobs = this.scheduler.pollJobsForCycle();
        int nextJobIndex = 0;
        boolean budgetOverrunRecorded = false;
        try {
            for (; nextJobIndex < jobs.size(); nextJobIndex++) {
                if (nextJobIndex > 0 && hasExceededTimeBudget(startTimeNanos)) {
                    if (this.profiler != null) {
                        this.profiler.recordBudgetOverrun();
                        budgetOverrunRecorded = true;
                    }
                    requeueJobs(jobs, nextJobIndex);
                    break;
                }

                RegionUpdateJob job = jobs.get(nextJobIndex);
                Optional<Region> region = this.regionManager.resolve(job.coordinate());
                if (region.isEmpty()) {
                    continue;
                }
                runModulesForRegion(region.get(), job, simulationTick);
            }

            this.timeService.advanceSimulationTick();
            this.scheduler.recordCompletedSimulationCycle();
        } finally {
            long durationMs = elapsedMilliseconds(startTimeNanos);
            this.scheduler.endCycle(durationMs);
            if (this.profiler != null) {
                if (!budgetOverrunRecorded
                        && durationMs > this.scheduler.getMaxMillisecondsPerCycle()) {
                    this.profiler.recordBudgetOverrun();
                }
                this.profiler.endCycle(durationMs);
            }
        }
    }

    private boolean hasExceededTimeBudget(long startTimeNanos) {
        return elapsedMilliseconds(startTimeNanos) >= this.scheduler.getMaxMillisecondsPerCycle();
    }

    private static long elapsedMilliseconds(long startTimeNanos) {
        return (System.nanoTime() - startTimeNanos) / 1_000_000L;
    }

    private void requeueJobs(List<RegionUpdateJob> jobs, int firstUnprocessedIndex) {
        for (int i = firstUnprocessedIndex; i < jobs.size(); i++) {
            this.scheduler.queueRegion(jobs.get(i));
        }
    }

    private void runModulesForRegion(Region region, RegionUpdateJob job, long simulationTick) {
        Set<String> requestedModules = job.requestedModules();
        boolean changed = false;

        for (SimulationModule module : this.moduleRegistry.getEnabledModules()) {
            if (!requestedModules.isEmpty() && !requestedModules.contains(module.getModuleId())) {
                continue;
            }

            try {
                if (this.profiler != null) {
                    this.profiler.beginModule(module.getModuleId());
                }
                ModuleUpdateResult result = module.updateRegion(new RegionUpdateContext(region));
                if (result == null) {
                    throw new IllegalStateException(
                            "Module " + module.getModuleId() + " returned a null update result");
                }

                changed |= result.changedRegion();
                result.generatedEvents().forEach(event -> {
                    this.eventBus.publish(event);
                    if (this.profiler != null) {
                        this.profiler.recordEvent();
                    }
                });
                result.warnings().forEach(warning -> LivingWorldLogger.warn(
                        DiagnosticCategory.SIMULATION,
                        "[" + module.getModuleId() + "] " + warning));
            } catch (Exception e) {
                LivingWorldLogger.error(DiagnosticCategory.SIMULATION,
                        "Module update failed for " + module.getModuleId()
                                + " in region " + region.getCoordinate(), e);
            } finally {
                if (this.profiler != null) {
                    this.profiler.endModule(module.getModuleId());
                }
            }
        }

        region.updateLastSimulatedTick(simulationTick);
        if (this.profiler != null) {
            this.profiler.recordRegionUpdated();
        }
        if (changed) {
            this.regionManager.markDirty(region);
        }
    }

    /**
     * Runs forced simulation ticks for debugging or testing.
     */
    public void runForcedSimulationTicks(int ticks) {
        if (ticks <= 0 || ticks > MAX_FORCED_SIMULATION_TICKS) {
            throw new IllegalArgumentException(
                String.format(
                        "forced ticks must be in range [1, %d], got: %d",
                        MAX_FORCED_SIMULATION_TICKS,
                        ticks));
        }

        for (int i = 0; i < ticks; i++) {
            runSimulationCycle();
        }
    }

    public long getMinecraftTickCounter() {
        return this.scheduler.getMinecraftTickCounter();
    }

    public long getSimulationTickCounter() {
        return this.scheduler.getSimulationTickCounter();
    }

    @Override
    public String toString() {
        return "SimulationManager{"
                + "minecraftTick=" + getMinecraftTickCounter()
                + ", simulationTick=" + getSimulationTickCounter()
                + '}';
    }
}
