<?php

namespace Modules\MedRegistry\Services;

use App\Utils\ModuleUtil;
use Modules\MedRegistry\Entities\MedregModule;
use Modules\MedRegistry\Entities\MedregModuleInstance;
use Modules\MedRegistry\Events\ModuleInstanceChanged;

/**
 * Instance lifecycle state machine over medreg_module_instances (P1-T2).
 *
 * This service ADDS the per-facility/department instance layer on top of the
 * existing gates and NEVER re-implements them (REUSE-MAP §6):
 *   - host install    → ModuleUtil::isModuleInstalled()  (system.{module}_version)
 *   - tenant package  → ModuleUtil::hasThePermissionInSubscription()
 *
 * Per decision D4 (TENANCY-PERMISSIONS-AUDIT): marketplace download/rollback is
 * out of scope; uninstall here deletes the instance row only — data is kept.
 * Host-level (un)install/update stays with each module's own /install routes.
 */
class ModuleLifecycleService
{
    /** @var ModuleUtil */
    protected $moduleUtil;

    /** Per-request cache of subscription package keys per module_key. */
    protected static $packageKeys = [];

    public function __construct(?ModuleUtil $moduleUtil = null)
    {
        $this->moduleUtil = $moduleUtil ?: new ModuleUtil();
    }

    /* ---------------------------------------------------------------------
     |  Instance resolution
     * -------------------------------------------------------------------*/

    /**
     * Resolve a catalog module by nwidart module_key first, then by spec_key
     * (with SPEC_ALIASES applied). Present modules win on spec_key ties;
     * sort_order breaks remaining ties (e.g. laboratory → LabCore before ClinicLab).
     */
    public function resolve(string $key): ?MedregModule
    {
        $key = trim($key);

        $module = MedregModule::where('module_key', $key)->first();
        if ($module) {
            return $module;
        }

        $specKey = MedregModule::SPEC_ALIASES[$key] ?? $key;

        return MedregModule::where('spec_key', $specKey)
            ->orderByDesc('is_present')
            ->orderBy('sort_order')
            ->orderBy('module_key')
            ->first();
    }

    /**
     * All PRESENT catalog modules serving a spec key (several host modules may
     * implement one spec module, e.g. LabCore + ClinicLab → laboratory).
     *
     * @return \Illuminate\Database\Eloquent\Collection|MedregModule[]
     */
    public function resolveAllPresent(string $key)
    {
        $key = trim($key);
        $specKey = MedregModule::SPEC_ALIASES[$key] ?? $key;

        $bySpec = MedregModule::where('spec_key', $specKey)
            ->where('is_present', true)
            ->orderBy('sort_order')
            ->orderBy('module_key')
            ->get();

        if ($bySpec->isNotEmpty()) {
            return $bySpec;
        }

        return MedregModule::where('module_key', $key)
            ->where('is_present', true)
            ->get();
    }

    /**
     * Get-or-new the instance row for a module × business × scope.
     */
    public function instance(int $moduleId, int $businessId, ?int $locationId = null, ?int $departmentId = null): MedregModuleInstance
    {
        return MedregModuleInstance::firstOrNew([
            'module_id'            => $moduleId,
            'business_id'          => $businessId,
            'business_location_id' => $locationId,
            'department_id'        => $departmentId,
        ]);
    }

    /* ---------------------------------------------------------------------
     |  State machine (install / enable / disable / softLock / uninstall)
     * -------------------------------------------------------------------*/

    /** Mark installed (data ready, not yet active for users). */
    public function install(MedregModuleInstance $instance): MedregModuleInstance
    {
        return $this->transition($instance, MedregModuleInstance::STATUS_INSTALLED, 'installed');
    }

    /** Activate: UI shown, API allowed. */
    public function enable(MedregModuleInstance $instance): MedregModuleInstance
    {
        return $this->transition($instance, MedregModuleInstance::STATUS_ENABLED, 'enabled');
    }

    /** Deactivate: hide UI / deny API — data kept. */
    public function disable(MedregModuleInstance $instance): MedregModuleInstance
    {
        return $this->transition($instance, MedregModuleInstance::STATUS_DISABLED, 'disabled');
    }

    /** Soft-lock: reject NEW operations, existing data readable (Specs §6). */
    public function softLock(MedregModuleInstance $instance): MedregModuleInstance
    {
        return $this->transition($instance, MedregModuleInstance::STATUS_SOFT_LOCKED, 'soft_locked');
    }

    /**
     * Uninstall the INSTANCE: delete the row, KEEP all module data (D4).
     * Host-level uninstall remains each module's own /uninstall route.
     */
    public function uninstall(MedregModuleInstance $instance): void
    {
        if (! $instance->exists) {
            return; // nothing to uninstall — idempotent
        }

        $previous = $instance->status;
        $instance->delete();

        event(new ModuleInstanceChanged($instance, 'uninstalled', $previous));
    }

    /** Apply a named action (used by controllers / console). */
    public function apply(MedregModuleInstance $instance, string $action): ?MedregModuleInstance
    {
        switch ($action) {
            case 'install':
                return $this->install($instance);
            case 'enable':
                return $this->enable($instance);
            case 'disable':
                return $this->disable($instance);
            case 'soft_lock':
                return $this->softLock($instance);
            case 'uninstall':
                $this->uninstall($instance);

                return null;
        }

        throw new \InvalidArgumentException("Unknown lifecycle action [{$action}]");
    }

    protected function transition(MedregModuleInstance $instance, string $status, string $action): MedregModuleInstance
    {
        $previous = $instance->exists ? $instance->status : null;

        $isNew = ! $instance->exists;
        $instance->status = $status;
        if (empty($instance->installed_at)) {
            $instance->installed_at = now();
        }
        $instance->save();

        if ($isNew || $previous !== $status) {
            event(new ModuleInstanceChanged($instance, $action, $previous));
        }

        return $instance;
    }

    /* ---------------------------------------------------------------------
     |  isEnabled — the single question everything else asks
     * -------------------------------------------------------------------*/

    /**
     * Is $specKeyOrModuleKey enabled for the business (optionally at a
     * location/department)?
     *
     * Resolution order (per Reuse-map §6):
     *  (1) host gate: module folder present + ModuleUtil::isModuleInstalled()
     *  (2) tenant subscription gate: hasThePermissionInSubscription() with the
     *      module's own package key when it declares one (fallback allow —
     *      mirroring ModuleUtil semantics when Superadmin is absent)
     *  (3) instance status chain: department → location → tenant (most specific
     *      wins). No instance row at any level → default DISABLED for
     *      medical-category modules (medical/specialty/facility), ENABLED for
     *      the rest — existing non-medical behavior never changes.
     */
    public function isEnabled(string $specKeyOrModuleKey, int $businessId, ?int $locationId = null, ?int $departmentId = null): bool
    {
        $module = $this->resolve($specKeyOrModuleKey);
        if (! $module) {
            return false;
        }

        // (1) Host gate — folder present; the system.{module}_version install
        // row is required only for modules that actually ship an install flow
        // (requires_install, detected from InstallController presence at sync
        // time). The 45 specialty plugins and other light modules never write
        // a version row, so presence alone gates them.
        if (! $module->is_present) {
            return false;
        }
        if ($module->requires_install && ! $this->moduleUtil->isModuleInstalled($module->module_key)) {
            return false;
        }

        // (2) Tenant subscription gate — only when the module declares its own
        // package key AND a user context exists (hasThePermissionInSubscription
        // dereferences auth()->user()). Errors fall back to allow, mirroring
        // the core's behavior on hosts without the Superadmin module.
        $packageKey = $this->packageKeyFor($module);
        if (! empty($packageKey) && auth()->check()) {
            try {
                if (! $this->moduleUtil->hasThePermissionInSubscription($businessId, $packageKey)) {
                    return false;
                }
            } catch (\Throwable $e) {
                // fallback allow
            }
        }

        // (3) Instance chain — most specific wins.
        $instances = MedregModuleInstance::where('module_id', $module->id)
            ->where('business_id', $businessId)
            ->get();

        $match = null;

        if (! empty($departmentId)) {
            $match = $instances->first(function ($i) use ($departmentId) {
                return (int) $i->department_id === (int) $departmentId;
            });
        }

        if (! $match && ! empty($locationId)) {
            $match = $instances->first(function ($i) use ($locationId) {
                return is_null($i->department_id)
                    && (int) $i->business_location_id === (int) $locationId;
            });
        }

        if (! $match) {
            $match = $instances->first(function ($i) {
                return is_null($i->department_id) && is_null($i->business_location_id);
            });
        }

        if ($match) {
            return $match->status === MedregModuleInstance::STATUS_ENABLED;
        }

        // No instance anywhere → category default.
        return ! $module->isMedical();
    }

    /**
     * The module's own Superadmin package key, discovered from its
     * DataController::superadmin_package() (e.g. Vrc → 'vrc_module').
     * Null when the module declares none.
     */
    public function packageKeyFor(MedregModule $module): ?string
    {
        $key = $module->module_key;

        if (array_key_exists($key, self::$packageKeys)) {
            return self::$packageKeys[$key];
        }

        $packageKey = null;

        try {
            $class = 'Modules\\' . $key . '\\Http\\Controllers\\DataController';
            if (class_exists($class)) {
                $object = new $class();
                if (method_exists($object, 'superadmin_package')) {
                    $packages = $object->superadmin_package();
                    if (is_array($packages) && ! empty($packages[0]['name'])) {
                        $packageKey = $packages[0]['name'];
                    }
                }
            }
        } catch (\Throwable $e) {
            $packageKey = null; // a broken DataController must never break gating
        }

        return self::$packageKeys[$key] = $packageKey;
    }
}
