<?php

namespace App\Models\Central;

use App\Models\AuditLog;
use App\Models\Branch;
use App\Models\Lookups\PaymentMethod;
use App\Models\Permission;
use App\Models\Role;
use App\Models\Sales\Till;
use App\Models\Scopes\TenantScope;
use App\Models\User;
use App\Services\Storage\TenantStorage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

/**
 * A tenant (customer organisation) in the single-database, row-level multi-tenancy
 * model. Every owned table carries a `tenant_id` filtered by {@see TenantScope}.
 *
 * Tenants are CENTRAL data: this model is never tenant-scoped itself. It is managed
 * exclusively from the landlord ("master control") panel under the `central` guard.
 */
class Tenant extends Model
{
    use SoftDeletes;

    public const STATUS_TRIAL = 'trial';

    public const STATUS_ACTIVE = 'active';

    public const STATUS_SUSPENDED = 'suspended';

    public const STATUS_CANCELLED = 'cancelled';

    public const STATUSES = [
        self::STATUS_TRIAL,
        self::STATUS_ACTIVE,
        self::STATUS_SUSPENDED,
        self::STATUS_CANCELLED,
    ];

    public const ENV_LIVE = 'live';

    public const ENV_TEST = 'test';

    public const ENVIRONMENTS = [
        self::ENV_LIVE,
        self::ENV_TEST,
    ];

    protected $fillable = [
        'name',
        'slug',
        'status',
        'environment',
        'parent_tenant_id',
        'last_synced_at',
        'last_synced_by',
        'plan_id',
        'trial_ends_at',
        'storage_used_bytes',
        'storage_files_count',
        'storage_calculated_at',
    ];

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'trial_ends_at' => 'datetime',
            'last_synced_at' => 'datetime',
            'storage_calculated_at' => 'datetime',
        ];
    }

    /**
     * Storage is no longer per tenant: every tenant shares the platform's one
     * Cloudflare R2 bucket and is separated by path prefix
     * ("<tenancy.storage_prefix>/<tenantId>/", see {@see TenantStorage}). The
     * `storage_source_id` column and the `storage_sources` /
     * `storage_migrations` tables are left in place on purpose — dropping them
     * is irreversible on a live database — but nothing reads them.
     *
     * The `storage_used_bytes` / `storage_files_count` / `storage_calculated_at`
     * columns are still live: they are the cached per-tenant consumption figures
     * shown in Master Control → Tenants and checked against the platform quota.
     */

    /**
     * The landlord-editable Branding / Invoice / Locale / Theme / POS config row.
     *
     * @return HasOne<TenantSetting, Tenant>
     */
    public function settingsRow(): HasOne
    {
        return $this->hasOne(TenantSetting::class);
    }

    /**
     * The tenant's branches, for the landlord panel's branch management
     * (docs/migration-plan.md Phase 10).
     *
     * Safe to query from the central panel: TenantScope passes through in
     * central context (see its resolution table), and this relationship's own
     * `warehouses.tenant_id = tenants.id` constraint provides the scoping —
     * so the landlord sees exactly one tenant's branches, never all of them.
     *
     * @return HasMany<Branch, Tenant>
     */
    public function branches(): HasMany
    {
        return $this->hasMany(Branch::class, 'tenant_id');
    }

    /**
     * Every user belonging to this tenant, for the landlord panel's Users
     * relation manager (there is no tenant scope to fight here — the central
     * guard already bypasses TenantScope, see TenantScope's isCentral() check).
     */
    public function users(): HasMany
    {
        return $this->hasMany(User::class, 'tenant_id');
    }

    /**
     * The tenant's roles, for the landlord panel's Roles relation manager —
     * same central-panel reasoning as {@see branches()}.
     *
     * @return HasMany<Role, Tenant>
     */
    public function roles(): HasMany
    {
        return $this->hasMany(Role::class, 'tenant_id');
    }

    /**
     * The tenant's tills — same central-panel reasoning as {@see branches()}.
     *
     * @return HasMany<Till, Tenant>
     */
    public function tills(): HasMany
    {
        return $this->hasMany(Till::class, 'tenant_id');
    }

    /**
     * The tenant's payment methods (built-in + custom), for the landlord
     * panel's Payment Methods relation manager — same central-panel reasoning
     * as {@see branches()}.
     *
     * @return HasMany<PaymentMethod, Tenant>
     */
    public function paymentMethods(): HasMany
    {
        return $this->hasMany(PaymentMethod::class, 'tenant_id');
    }

    /**
     * This tenant's audit trail, for the landlord panel's Audit Logs relation
     * manager — same central-panel reasoning as {@see branches()}: TenantScope
     * passes through in central context, and this relationship's own
     * `audit_logs.tenant_id = tenants.id` constraint provides the scoping, so
     * the landlord sees exactly one tenant's log, never every tenant's at once.
     *
     * @return HasMany<AuditLog, Tenant>
     */
    public function auditLogs(): HasMany
    {
        return $this->hasMany(AuditLog::class, 'tenant_id');
    }

    protected static function booted(): void
    {
        // Every tenant gets an (initially empty) settings row so settingsRow()
        // is never null — consumers don't need to guard against a missing row.
        static::created(function (Tenant $tenant): void {
            $tenant->settingsRow()->create([]);
        });
    }

    /**
     * Permissions this tenant is entitled to use — the ceiling above the
     * per-tenant RBAC. A tenant's own roles/users can never exceed this set.
     */
    public function entitlements(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, 'tenant_permissions', 'tenant_id', 'permission_id')
            ->withTimestamps();
    }

    /**
     * Cached set of entitled permission names (e.g. "isms_invoices.create").
     * Flushed by the landlord whenever the entitlement matrix is saved.
     *
     * @return array<int, string>
     */
    public function entitledPermissionNames(): array
    {
        return self::entitledPermissionNamesFor($this->id);
    }

    /**
     * Cached entitled permission names for a tenant id, without needing to load
     * the model — used in the hot permission-check path.
     *
     * @return array<int, string>
     */
    public static function entitledPermissionNamesFor(int $tenantId): array
    {
        return Cache::remember(
            self::entitlementCacheKey($tenantId),
            now()->addHour(),
            fn (): array => DB::table('tenant_permissions as tp')
                ->join('permissions as p', 'p.id', '=', 'tp.permission_id')
                ->where('tp.tenant_id', $tenantId)
                ->pluck('p.name')
                ->all(),
        );
    }

    public static function entitlementCacheKey(int $tenantId): string
    {
        return "tenant:{$tenantId}:entitlements";
    }

    public function flushEntitlementCache(): void
    {
        Cache::forget(self::entitlementCacheKey($this->id));
        Cache::forget(self::settingsCacheKey($this->id));
    }

    public static function settingsCacheKey(int $tenantId): string
    {
        return "tenant:{$tenantId}:settings";
    }

    /**
     * Read a single branding/config setting with a fallback. $key is a dot
     * path into the nested shape, e.g. "branding.logo_url".
     */
    public function setting(string $key, mixed $default = null): mixed
    {
        return data_get($this->settingsRow?->toNestedArray() ?? [], $key, $default);
    }

    /**
     * The live tenant this test instance mirrors. Null on live rows.
     *
     * @return BelongsTo<Tenant, Tenant>
     */
    public function parentTenant(): BelongsTo
    {
        return $this->belongsTo(self::class, 'parent_tenant_id');
    }

    /**
     * This tenant's test instance, if one was allocated.
     *
     * @return HasOne<Tenant, Tenant>
     */
    public function testInstance(): HasOne
    {
        return $this->hasOne(self::class, 'parent_tenant_id')->where('environment', self::ENV_TEST);
    }

    /**
     * The central operator who last ran "Sync from Live", when recorded.
     *
     * @return BelongsTo<CentralAdmin, Tenant>
     */
    public function lastSyncAdmin(): BelongsTo
    {
        return $this->belongsTo(CentralAdmin::class, 'last_synced_by');
    }

    /**
     * Whether this row is a test instance rather than the live environment.
     */
    public function isTestInstance(): bool
    {
        return $this->environment === self::ENV_TEST;
    }

    public function environmentLabel(): string
    {
        return $this->isTestInstance() ? 'Test' : 'Live';
    }

    /**
     * @param  Builder<Tenant>  $query
     */
    public function scopeLive(Builder $query): void
    {
        $query->where('environment', self::ENV_LIVE);
    }

    /**
     * @param  Builder<Tenant>  $query
     */
    public function scopeTestInstances(Builder $query): void
    {
        $query->where('environment', self::ENV_TEST);
    }

    public function isActive(): bool
    {
        return in_array($this->status, [self::STATUS_TRIAL, self::STATUS_ACTIVE], true);
    }

    /**
     * The tenant's own URL. With tenancy.routing = "path" (the production
     * strategy; see docs/path-based-tenancy-plan.md) this is
     * "{app.url}/<slug><path>" (default path "/app"), e.g.
     * "https://pos.com/wasana/app". With "subdomain" it is the host-form
     * "https://<slug>.<app_domain><path>", preserving a non-standard port, as
     * the source app builds it.
     *
     * @param  string  $path  Path below the tenant root, e.g. "/app" or
     *                        "/impersonate/<token>".
     */
    public function url(string $path = '/app'): string
    {
        if (config('tenancy.routing') === 'path') {
            return url('/'.$this->slug.$path);
        }

        $request = request();
        $scheme = $request->getScheme();
        $port = (int) $request->getPort();
        $standardPorts = ['http' => 80, 'https' => 443];
        $portSuffix = ($port && $port !== ($standardPorts[$scheme] ?? null)) ? ":{$port}" : '';

        return "{$scheme}://{$this->slug}.".config('tenancy.app_domain').$portSuffix.$path;
    }

    public function isSuspended(): bool
    {
        return $this->status === self::STATUS_SUSPENDED;
    }

    /**
     * @param  Builder<Tenant>  $query
     */
    public function scopeActive(Builder $query): void
    {
        $query->whereIn('status', [self::STATUS_TRIAL, self::STATUS_ACTIVE]);
    }
}
