<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
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\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class Server extends Model
{
    use HasFactory, SoftDeletes;

    public const STATUS_ONLINE = 'online';
    public const STATUS_WARNING = 'warning';
    public const STATUS_OFFLINE = 'offline';
    public const STATUS_MAINTENANCE = 'maintenance';

    // AI Portal status vocabulary — autonomous-audit findings drive these.
    // Coexists with the NOC monitoring status (online/warning/...) above so
    // both pipelines can write to the same row without overwriting each other.
    public const STATUS_HEALTHY  = 'healthy';
    public const STATUS_DEGRADED = 'degraded';
    public const STATUS_CRITICAL = 'critical';
    public const STATUS_PENDING  = 'pending';

    public const AI_MODE_MANUAL   = 'manual';
    public const AI_MODE_ASSISTED = 'assisted';
    public const AI_MODE_AUTO     = 'auto-whitelist';

    protected $fillable = [
        'name', 'hostname', 'ip', 'location', 'provider', 'type', 'status',
        'monitoring_mode', 'check_interval_seconds',
        'manual_check_method', 'manual_check_port', 'manual_check_path', 'manual_timeout_seconds',
        // Backoff + circuit breaker for manual probes (set by ManualPingService)
        'manual_consecutive_failures', 'manual_next_check_at',
        'monitor_blocked', 'monitor_blocked_at', 'monitor_blocked_reason',
        // Multi-location consensus (set by ConsensusService)
        'locations_total', 'locations_reporting_down', 'consensus_status', 'consensus_updated_at',
        'tags', 'os', 'kernel', 'control_panel', 'web_server',
        'ssh_port', 'ssh_user', 'ssh_key_encrypted',
        'cpu', 'ram', 'disk', 'load_1m', 'load_5m', 'load_15m',
        'net_in_mbps', 'net_out_mbps', 'services', 'uptime_human', 'uptime_seconds',
        'last_seen_at', 'last_metric_at',
        'is_active', 'alerts_enabled', 'created_by',
        // AI Portal additions
        'ai_mode', 'auto_fix_categories', 'ai_settings',
        'audit_interval_minutes', 'last_audit_at', 'last_audit_status',
        'last_audit_error', 'audit_failures_in_row',
        'agent_public_key', 'agent_handshake_at', 'last_heartbeat_at',
        'pending_issues_count',
        'ssh_password',
        'host_key_fingerprint', 'passwordless_mode',
        'stack', 'stack_detected_at',
        'noc_server_id', 'noc_metadata',
    ];

    // AI Portal added these to $hidden — keep ssh_password / ssh_key out of
    // serialised responses by default; the controller opts in explicitly.
    protected $hidden = [
        'ssh_password', 'ssh_key_encrypted',
    ];

    protected function casts(): array
    {
        return [
            'tags' => 'array',
            'services' => 'array',
            'cpu' => 'decimal:2',
            'ram' => 'decimal:2',
            'disk' => 'decimal:2',
            'load_1m' => 'decimal:2',
            'load_5m' => 'decimal:2',
            'load_15m' => 'decimal:2',
            'net_in_mbps' => 'decimal:2',
            'net_out_mbps' => 'decimal:2',
            'last_seen_at' => 'datetime',
            'last_metric_at' => 'datetime',
            // Backoff timestamps must be Carbon so isFuture() etc. work in the service
            'manual_next_check_at' => 'datetime',
            'monitor_blocked_at' => 'datetime',
            'monitor_blocked' => 'boolean',
            'consensus_updated_at' => 'datetime',
            'is_active' => 'boolean',
            'alerts_enabled' => 'boolean',
            'ssh_key_encrypted' => 'encrypted',
            // AI Portal additions
            'auto_fix_categories'  => 'array',
            'ai_settings'          => 'array',
            'agent_handshake_at'   => 'datetime',
            'last_heartbeat_at'    => 'datetime',
            // Was missing — ServerResource called ->toIso8601String() on
            // this assuming Carbon, but with no cast it came back as a
            // raw string from the DB and threw "Call to a member function
            // toIso8601String() on string" on every agent-mode server
            // list response. Added when fixing the Mail Queue page bug.
            'agent_last_push_at'   => 'datetime',
            'last_audit_at'        => 'datetime',
            'stack'                => 'array',
            'stack_detected_at'    => 'datetime',
            'noc_metadata'         => 'array',
            'passwordless_mode'    => 'boolean',
            'ssh_password'         => 'encrypted',
        ];
    }

    public function aiActions(): HasMany { return $this->hasMany(AiAction::class); }
    public function issues(): HasMany    { return $this->hasMany(Issue::class); }

    public function hasSshCredentials(): bool
    {
        // NOC stores the key in ssh_key_encrypted (auto-decrypts via cast).
        // AI Portal's onboarding writes ssh_password (also encrypted).
        return $this->ssh_user && ($this->ssh_password || $this->ssh_key_encrypted);
    }

    public function effectiveAutoFixCategories(): array
    {
        return $this->auto_fix_categories
            ?? config('hsap.auto_fix.allowed_categories', []);
    }

    /**
     * Heartbeat liveness — derived; not the stored status field which lags.
     */
    public function isStale(): bool
    {
        return ! $this->last_heartbeat_at
            || $this->last_heartbeat_at->lt(now()->subMinutes(2));
    }

    public function metrics(): HasMany
    {
        return $this->hasMany(ServerMetric::class);
    }

    /**
     * Probe locations explicitly assigned to check this server. If empty,
     * the consensus job assumes "all active locations" — that's the
     * implicit default for a freshly-added server.
     */
    public function probeLocations(): BelongsToMany
    {
        return $this->belongsToMany(ProbeLocation::class, 'server_probe_locations')->withTimestamps();
    }

    public function locationCheckResults()
    {
        return $this->morphMany(LocationCheckResult::class, 'target');
    }

    public function uptimeBuckets()
    {
        return $this->morphMany(UptimeBucket::class, 'target');
    }

    public function ports(): HasMany
    {
        return $this->hasMany(Port::class);
    }

    public function websites(): HasMany
    {
        return $this->hasMany(Website::class);
    }

    public function emailMonitors(): HasMany
    {
        return $this->hasMany(EmailMonitor::class);
    }

    /* Hosting-specific telemetry relationships */
    public function backupRuns(): HasMany       { return $this->hasMany(BackupRun::class); }
    public function diskPartitions(): HasMany   { return $this->hasMany(DiskPartition::class); }
    public function emailQueueSnapshots(): HasMany { return $this->hasMany(EmailQueueSnapshot::class); }
    public function emailSendStats(): HasMany   { return $this->hasMany(EmailSendStat::class); }
    public function mysqlSnapshots(): HasMany   { return $this->hasMany(MysqlSnapshot::class); }
    public function cpanelSnapshots(): HasMany  { return $this->hasMany(CpanelSnapshot::class); }
    public function litespeedSnapshots(): HasMany { return $this->hasMany(LitespeedSnapshot::class); }
    public function firewallEvents(): HasMany   { return $this->hasMany(FirewallEvent::class); }
    public function processChecks(): HasMany    { return $this->hasMany(ProcessCheck::class); }
    public function cronChecks(): HasMany       { return $this->hasMany(CronCheck::class); }

    public function incidents(): MorphMany
    {
        return $this->morphMany(Incident::class, 'subject');
    }

    public function creator(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by');
    }

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

    /**
     * True when this server runs the agent (gets full metrics) vs being
     * pinged externally (up/down only).
     */
    public function hasAgent(): bool
    {
        return ($this->monitoring_mode ?? 'agent') === 'agent'
            && !empty($this->agent_secret_hash);
    }

    public function isManualMode(): bool
    {
        return ($this->monitoring_mode ?? 'agent') === 'manual';
    }

    public function scopeActive($q)
    {
        return $q->where('is_active', true);
    }

    public function scopeOnline($q)
    {
        return $q->where('status', self::STATUS_ONLINE);
    }

    public function scopeAgentMode($q)
    {
        return $q->where('monitoring_mode', 'agent');
    }

    public function scopeManualMode($q)
    {
        return $q->where('monitoring_mode', 'manual');
    }
}
