<?php

declare(strict_types=1);

namespace App\Models;

use App\Traits\MultiTenantScope;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Cache;

/**
 * Company Subscription Model.
 *
 * Tracks a company's subscription to a plan, including limits,
 * billing cycle, and status.
 *
 * STATUS FLOW:
 * trial -> active -> grace -> expired
 *                 -> canceled
 *                 -> suspended
 *
 * LIMIT RESOLUTION:
 * 1. Check custom_limits first (overrides)
 * 2. Fall back to plan limits
 * 3. 0 = unlimited
 *
 * @property int $id
 * @property int $company_id
 * @property int|null $plan_id
 * @property string $billing_cycle
 * @property Carbon $starts_at
 * @property Carbon $ends_at
 * @property string $status
 * @property Carbon|null $trial_ends_at
 * @property Carbon|null $grace_period_ends_at
 * @property string|null $amount_paid
 * @property string|null $payment_reference
 * @property array|null $custom_limits
 * @property bool $is_active
 * @property Carbon|null $canceled_at
 * @property string|null $cancellation_reason
 * @property int|null $canceled_by_user_id
 * @property string|null $notes
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @property-read Company $company
 * @property-read Plan|null $plan
 * @property-read User|null $canceledBy
 */
class Subscription extends Model
{
    use HasFactory, MultiTenantScope;

    // ═══════════════════════════════════════════════════════════════════════════
    // CONSTANTS
    // ═══════════════════════════════════════════════════════════════════════════

    public const STATUS_ACTIVE = 'active';

    public const STATUS_TRIAL = 'trial';

    public const STATUS_GRACE = 'grace';

    public const STATUS_EXPIRED = 'expired';

    public const STATUS_CANCELED = 'canceled';

    public const STATUS_SUSPENDED = 'suspended';

    public const BILLING_MONTHLY = 'monthly';

    public const BILLING_QUARTERLY = 'quarterly';

    public const BILLING_SEMIANNUAL = 'semiannual';

    public const BILLING_ANNUAL = 'annual';

    // Grace period in days after expiration
    public const GRACE_PERIOD_DAYS = 7;

    protected $fillable = [
        'company_id',
        'plan_id',
        'billing_cycle',
        'starts_at',
        'ends_at',
        'status',
        'trial_ends_at',
        'grace_period_ends_at',
        'amount_paid',
        'payment_reference',
        'custom_limits',
        'is_active',
        'canceled_at',
        'cancellation_reason',
        'canceled_by_user_id',
        'notes',
    ];

    protected $casts = [
        'starts_at' => 'datetime',
        'ends_at' => 'datetime',
        'trial_ends_at' => 'datetime',
        'grace_period_ends_at' => 'datetime',
        'canceled_at' => 'datetime',
        'amount_paid' => 'decimal:2',
        'custom_limits' => 'array',
        'is_active' => 'boolean',
    ];

    protected $attributes = [
        'status' => self::STATUS_ACTIVE,
        'billing_cycle' => self::BILLING_MONTHLY,
        'is_active' => true,
    ];

    // ═══════════════════════════════════════════════════════════════════════════
    // MODEL EVENTS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Invalida la cache de estado de suscripción de la company al guardar/eliminar.
     *
     * User::hasActiveSubscription() cachea el resultado del EXISTS por company_id
     * (lo consulta EnsurePwaAccess en cada request PWA). Cualquier cambio de
     * suscripción —alta, renovación, cancelación, expiración— debe reflejarse de
     * inmediato sin esperar al TTL; por eso limpiamos la key aquí.
     */
    protected static function booted(): void
    {
        $invalidate = static function (self $subscription): void {
            Cache::forget(User::subscriptionCacheKey($subscription->company_id));
        };

        static::saved($invalidate);
        static::deleted($invalidate);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // RELATIONSHIPS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * The company this subscription belongs to.
     */
    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    /**
     * The plan associated with this subscription.
     */
    public function plan(): BelongsTo
    {
        return $this->belongsTo(Plan::class);
    }

    /**
     * User who canceled the subscription.
     */
    public function canceledBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'canceled_by_user_id');
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // SCOPES
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Active subscriptions (not expired, canceled, or suspended).
     */
    public function scopeActive($query)
    {
        return $query->whereIn('status', [
            self::STATUS_ACTIVE,
            self::STATUS_TRIAL,
            self::STATUS_GRACE,
        ]);
    }

    /**
     * Current subscription for a company.
     */
    public function scopeCurrent($query)
    {
        // El OR debe ir agrupado: sin el closure, encadenado tras scopeActive()
        // (`status IN (...)`) la SQL queda `status IN (...) AND ends_at>=now OR
        // grace_period_ends_at>=now` y el OR escapa del grupo, haciendo que una
        // suscripción cancelada/expirada con grace_period_ends_at futuro matchee.
        //
        // La gracia se computa EN VIVO (`ends_at >= now - GRACE_PERIOD_DAYS`), no
        // solo desde `grace_period_ends_at`: esa columna queda NULL hasta que el
        // cron nocturno la materializa, así que un trial/active recién vencido
        // (a cualquier hora) quedaría sin acceso hasta las 00:00 pese a tener
        // gracia vigente (#164). La condición en vivo cubre esa ventana; sigue
        // acotada por scopeActive() (status IN active/trial/grace), de modo que
        // una suscripción ya expirada/cancelada no se cuela.
        return $query->where(function ($q) {
            $q->where('ends_at', '>=', now())
                ->orWhere('grace_period_ends_at', '>=', now())
                ->orWhere('ends_at', '>=', now()->subDays(self::GRACE_PERIOD_DAYS));
        });
    }

    /**
     * Expired subscriptions needing attention.
     */
    public function scopeExpired($query)
    {
        return $query->where('status', self::STATUS_EXPIRED);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // STATUS CHECKS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Is subscription currently active (usable)?
     */
    public function isActive(): bool
    {
        return in_array($this->status, [
            self::STATUS_ACTIVE,
            self::STATUS_TRIAL,
            self::STATUS_GRACE,
        ]);
    }

    /**
     * Is in trial period?
     */
    public function isOnTrial(): bool
    {
        return $this->status === self::STATUS_TRIAL
            && $this->trial_ends_at
            && $this->trial_ends_at->isFuture();
    }

    /**
     * Is in grace period?
     */
    public function isInGracePeriod(): bool
    {
        return $this->status === self::STATUS_GRACE
            && $this->grace_period_ends_at
            && $this->grace_period_ends_at->isFuture();
    }

    /**
     * Is subscription expired?
     */
    public function isExpired(): bool
    {
        return $this->status === self::STATUS_EXPIRED
            || ($this->ends_at && $this->ends_at->isPast() && ! $this->isInGracePeriod());
    }

    /**
     * Is subscription canceled?
     */
    public function isCanceled(): bool
    {
        return $this->status === self::STATUS_CANCELED;
    }

    /**
     * Is subscription suspended by admin?
     */
    public function isSuspended(): bool
    {
        return $this->status === self::STATUS_SUSPENDED;
    }

    /**
     * Days until expiration (negative if expired).
     */
    public function daysUntilExpiration(): int
    {
        if (! $this->ends_at) {
            return 999; // Far future
        }

        return (int) now()->diffInDays($this->ends_at, false);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // LIMIT RESOLUTION
    // Custom limits override plan limits; 0 = unlimited
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Get effective limit for a field.
     * Priority: custom_limits > plan limits
     */
    public function getLimit(string $field): int|float
    {
        $value = null;

        // Check custom overrides first
        if ($this->custom_limits && isset($this->custom_limits[$field])) {
            $value = $this->custom_limits[$field];
        } elseif ($this->plan) {
            // Fall back to plan limits
            $value = $this->plan->{$field} ?? 0;
        }

        if ($value === null) {
            return 0; // Default to unlimited if no plan
        }

        // Cast to appropriate numeric type
        if (is_numeric($value)) {
            return str_contains((string) $value, '.') ? (float) $value : (int) $value;
        }

        return 0;
    }

    /**
     * Get max users limit.
     */
    public function getMaxUsers(): int
    {
        return (int) $this->getLimit('max_users');
    }

    /**
     * Get max collectors limit.
     */
    public function getMaxCollectors(): int
    {
        return (int) $this->getLimit('max_collectors');
    }

    /**
     * Get max active credits limit.
     */
    public function getMaxActiveCredits(): int
    {
        return (int) $this->getLimit('max_active_credits');
    }

    /**
     * Get max monthly volume limit.
     */
    public function getMaxMonthlyVolume(): float
    {
        return (float) $this->getLimit('max_monthly_volume');
    }

    /**
     * Get max clients limit.
     */
    public function getMaxClients(): int
    {
        return (int) $this->getLimit('max_clients');
    }

    /**
     * Check if a limit is unlimited (0).
     */
    public function isUnlimited(string $field): bool
    {
        return $this->getLimit($field) == 0;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // FEATURE CHECKS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Check if subscription has a specific feature.
     */
    public function hasFeature(string $feature): bool
    {
        // Check plan features
        if ($this->plan) {
            // Check boolean flags first
            $flagField = 'has_'.$feature;
            if (isset($this->plan->{$flagField})) {
                return (bool) $this->plan->{$flagField};
            }

            // Check features JSON
            return $this->plan->hasFeature($feature);
        }

        return false;
    }

    /**
     * Has PWA access?
     */
    public function hasPwaAccess(): bool
    {
        return $this->plan?->has_pwa_access ?? true;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // USAGE TRACKING HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Check if company can add more users.
     */
    public function canAddUser(int $currentCount): bool
    {
        $max = $this->getMaxUsers();

        return $max === 0 || $currentCount < $max;
    }

    /**
     * Check if company can add more credits.
     */
    public function canAddCredit(int $currentActiveCount): bool
    {
        $max = $this->getMaxActiveCredits();

        return $max === 0 || $currentActiveCount < $max;
    }

    /**
     * Check if company can disburse more volume this month.
     */
    public function canDisburse(float $currentMonthlyVolume, float $amount): bool
    {
        $max = $this->getMaxMonthlyVolume();

        return $max == 0 || ($currentMonthlyVolume + $amount) <= $max;
    }

    /**
     * Get usage percentage for a limit.
     */
    public function getUsagePercentage(string $field, int|float|string $currentUsage): int
    {
        $max = $this->getLimit($field);

        if ($max == 0) {
            return 0; // Unlimited
        }

        // Cast to float to handle string values from decimal columns
        $usage = is_numeric($currentUsage) ? (float) $currentUsage : 0;

        return min(100, (int) round(($usage / $max) * 100));
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // STATUS TRANSITIONS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Activate the subscription.
     */
    public function activate(): bool
    {
        $this->status = self::STATUS_ACTIVE;
        $this->is_active = true;

        return $this->save();
    }

    /**
     * Start grace period.
     */
    public function startGracePeriod(): bool
    {
        $this->status = self::STATUS_GRACE;
        // La gracia se cuenta desde el vencimiento (ends_at), no desde "ahora":
        // así la ventana [ends_at, ends_at+7d] es idempotente y una suscripción
        // vencida hace mucho no resucita con 7 días nuevos (queda con la gracia
        // ya lapsada y el comando la expira en la misma pasada).
        $this->grace_period_ends_at = $this->ends_at->copy()->addDays(self::GRACE_PERIOD_DAYS);

        return $this->save();
    }

    /**
     * Mark as expired.
     */
    public function expire(): bool
    {
        $this->status = self::STATUS_EXPIRED;
        $this->is_active = false;

        return $this->save();
    }

    /**
     * Cancel the subscription.
     */
    public function cancel(?int $userId = null, ?string $reason = null): bool
    {
        $this->status = self::STATUS_CANCELED;
        $this->is_active = false;
        $this->canceled_at = now();
        $this->canceled_by_user_id = $userId;
        $this->cancellation_reason = $reason;

        return $this->save();
    }

    /**
     * Suspend by admin.
     */
    public function suspend(?string $reason = null): bool
    {
        $this->status = self::STATUS_SUSPENDED;
        $this->is_active = false;
        $this->notes = $reason ? "Suspended: {$reason}" : $this->notes;

        return $this->save();
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // ACCESSORS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Get status label for display.
     */
    public function getStatusLabelAttribute(): string
    {
        return match ($this->status) {
            self::STATUS_ACTIVE => 'Activo',
            self::STATUS_TRIAL => 'Prueba',
            self::STATUS_GRACE => 'Período de Gracia',
            self::STATUS_EXPIRED => 'Expirado',
            self::STATUS_CANCELED => 'Cancelado',
            self::STATUS_SUSPENDED => 'Suspendido',
            default => ucfirst($this->status),
        };
    }

    /**
     * Get status badge color.
     */
    public function getStatusColorAttribute(): string
    {
        return match ($this->status) {
            self::STATUS_ACTIVE => 'success',
            self::STATUS_TRIAL => 'info',
            self::STATUS_GRACE => 'warning',
            self::STATUS_EXPIRED, self::STATUS_CANCELED, self::STATUS_SUSPENDED => 'danger',
            default => 'gray',
        };
    }

    /**
     * Get billing cycle label.
     */
    public function getBillingCycleLabelAttribute(): string
    {
        return match ($this->billing_cycle) {
            self::BILLING_MONTHLY => 'Mensual',
            self::BILLING_QUARTERLY => 'Trimestral',
            self::BILLING_SEMIANNUAL => 'Semestral',
            self::BILLING_ANNUAL => 'Anual',
            default => ucfirst($this->billing_cycle),
        };
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // STATIC HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Get current active subscription for a company.
     */
    public static function currentForCompany(int $companyId): ?self
    {
        return static::where('company_id', $companyId)
            ->active()
            ->current()
            ->latest('ends_at')
            ->first();
    }

    /**
     * Create a trial subscription.
     */
    public static function createTrial(int $companyId, int $planId, int $trialDays = 14): self
    {
        return static::create([
            'company_id' => $companyId,
            'plan_id' => $planId,
            'billing_cycle' => self::BILLING_MONTHLY,
            'status' => self::STATUS_TRIAL,
            'starts_at' => now(),
            'ends_at' => now()->addDays($trialDays),
            'trial_ends_at' => now()->addDays($trialDays),
            'is_active' => true,
        ]);
    }
}
