<?php

namespace App\Models;

use App\Models\Concerns\HasUuid;
use App\Services\Billing\StorageAllowance;
use Database\Factories\InstanceFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Instance extends Model
{
    /** @use HasFactory<InstanceFactory> */
    use HasFactory, HasUuid;

    protected $fillable = [
        'customer_id', 'order_id', 'host_id', 'vmid', 'guest_ip', 'plan', 'quota_gb', 'quota_applied_gb', 'traffic_addons', 'disk_gb',
        'ram_mb', 'cores', 'restart_required_since',
        'subdomain', 'custom_domain', 'nc_admin_ref', 'admin_password', 'credentials_acknowledged_at',
        'route_written', 'routed_hostnames', 'routed_backend', 'cert_ok', 'status', 'suspended_at', 'cancel_requested_at', 'service_ends_at',
        'export_wish', 'export_reminded_at',
        'domain_token', 'domain_verified_at', 'domain_cert_ok', 'domain_checked_at', 'domain_error', 'domain_failures',
        'security_log_offset',
    ];

    protected $hidden = ['nc_admin_ref', 'admin_password'];

    protected function casts(): array
    {
        return [
            'nc_admin_ref' => 'encrypted',
            // The initial Nextcloud admin password, held only until the
            // customer acknowledges it on the dashboard (App\Livewire\
            // Dashboard::acknowledgeCredentials). Same 'encrypted' cast as
            // nc_admin_ref and Host::api_token_ref, for the same reason.
            'admin_password' => 'encrypted',
            'credentials_acknowledged_at' => 'datetime',
            'suspended_at' => 'datetime',
            'domain_verified_at' => 'datetime',
            'domain_checked_at' => 'datetime',
            'domain_failures' => 'integer',
            'security_log_offset' => 'integer',
            'domain_cert_ok' => 'boolean',
            'route_written' => 'boolean',
            'routed_hostnames' => 'array',
            'cert_ok' => 'boolean',
            'vmid' => 'integer',
            'traffic_addons' => 'integer',
            'quota_gb' => 'integer',
            'quota_applied_gb' => 'integer',
            'disk_gb' => 'integer',
            'ram_mb' => 'integer',
            'cores' => 'integer',
            'cancel_requested_at' => 'datetime',
            'service_ends_at' => 'datetime',
            // export_wish fuehrt DREI Zustaende: null (nicht gefragt), true, false.
            // Laravels boolean-Cast laesst null als null durch (kein (bool)null),
            // deshalb unterstellt eine Bestandsinstanz ohne Antwort kein "nein".
            'export_wish' => 'boolean',
            'export_reminded_at' => 'datetime',
            'restart_required_since' => 'datetime',
        ];
    }

    /**
     * Is this machine running on less CPU or RAM than it has been sold?
     *
     * Proxmox writes a new `cores`/`memory` into the VM's configuration
     * immediately and a running guest picks it up at its next cold boot. A plan
     * change therefore leaves an honest gap, and rebooting somebody's cloud as a
     * side effect of a purchase is not a way to close it — so the gap is stated
     * instead, here, and shown to the operator and to the customer.
     */
    public function restartIsPending(): bool
    {
        return $this->restart_required_since !== null;
    }

    /**
     * Has this machine's Nextcloud actually been told the allowance that was
     * sold?
     *
     * Two figures rather than one because they answer two different questions:
     * what the customer is OWED, and what the guest was last TOLD. They agree on
     * every machine the quota step has visited since it started recording, and
     * they disagree on exactly the three cases worth finding — the estate built
     * before the step existed, any machine whose package changed while the run
     * that should have followed did not finish, and one whose storage packs were
     * booked or cancelled without the delivery run reaching the end.
     *
     * What is owed is asked of StorageAllowance and not read off `quota_gb`:
     * that column is the package alone, and a customer with a booked pack is
     * owed more than their package includes.
     */
    public function quotaIsEnforced(): bool
    {
        $owed = StorageAllowance::for($this)->totalGb();

        return $owed > 0 && (int) $this->quota_applied_gb === $owed;
    }

    /**
     * Is there a machine here that remote work can actually reach?
     *
     * A reservation with no VM, a failed build and an instance whose order has
     * gone all fail the same way: the steps reach for a guest agent that is not
     * there and burn a run's retries doing it. An instance that is still
     * `provisioning` is excluded for a different reason — its own run is already
     * writing this machine, and a second run beside it would write the same
     * router and race the same occ calls.
     *
     * Asked by everything that starts a maintenance run against a live instance
     * (ReapplyInstanceAddress, ApplyPlanChange), so the rule is one rule.
     */
    public function hasLiveMachine(): bool
    {
        return $this->order !== null
            && $this->host !== null
            && $this->vmid !== null
            && in_array($this->status, ['active', 'cancellation_scheduled'], true);
    }

    /**
     * Instances that are actually occupying storage on their host.
     *
     * A failed instance still holds its disk while its VM exists; a failure
     * before any VM was created releases it. Kept here rather than written out
     * at each call site so the host's own accounting and anything reporting on
     * it cannot drift apart — a dashboard that counted differently from
     * placement would call a host comfortable while orders were being refused
     * on it.
     *
     * @param  Builder<self>  $query
     */
    public function scopeOccupyingHost($query): void
    {
        $query->where(fn ($q) => $q->where('status', '!=', 'failed')->orWhereNotNull('vmid'));
    }

    /**
     * Is the customer's own domain proven to be theirs?
     *
     * The one flag. A domain that has never been verified — or that lost its
     * DNS proof and was withdrawn — is not this instance's address, whatever is
     * typed in the column.
     */
    public function domainIsVerified(): bool
    {
        return filled($this->custom_domain) && $this->domain_verified_at !== null;
    }

    /**
     * Is the customer's own domain not merely proven, but actually answering?
     *
     * Verification says the domain is theirs. It says nothing about whether
     * they have pointed it at us — that is an A record in their zone, which we
     * cannot create and (behind a CDN) cannot even read. So there is a real
     * state between "proven" and "working", it can last forever, and the portal
     * has to be able to say which one the customer is in rather than printing
     * an address that answers nothing.
     */
    public function domainIsServed(): bool
    {
        return $this->domainIsVerified() && (bool) $this->domain_cert_ok;
    }

    /**
     * The address this instance is actually served at.
     *
     * Every caller goes through here rather than reading `custom_domain ?:
     * subdomain` for itself. That expression was the hole: it served, certified
     * and trusted whatever hostname was in the column, so anybody who pointed
     * any domain at the proxy got somebody else's Nextcloud under their own
     * name — with a certificate to match.
     */
    public function address(string $zone): string
    {
        return $this->domainIsVerified()
            ? (string) $this->custom_domain
            : $this->subdomain.'.'.$zone;
    }

    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * The contract this machine fulfils. The authority on what the customer is
     * entitled to — the catalogue only describes what we sell today.
     */
    public function subscription(): HasOne
    {
        return $this->hasOne(Subscription::class);
    }

    public function host(): BelongsTo
    {
        return $this->belongsTo(Host::class);
    }

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

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

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

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