<?php

namespace App\Models;

use App\Exceptions\IdentityCollisionException;
use App\Models\Concerns\HasUuid;
use Database\Factories\CustomerFactory;
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\UniqueConstraintViolationException;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class Customer extends Model
{
    /** Somebody buying for private purposes. Has the statutory withdrawal right. */
    public const TYPE_CONSUMER = 'consumer';

    /** Somebody buying for their business. Has no withdrawal right. */
    public const TYPE_BUSINESS = 'business';

    /** The two answers a person may give. NULL — never asked — is not one of them. */
    public const TYPES = [self::TYPE_CONSUMER, self::TYPE_BUSINESS];

    /**
     * The customer behind a signed-in portal account.
     *
     * Here rather than in the Livewire concern that used to own it, because the
     * navigation gate has to answer the same question outside any component,
     * and a second copy of the fallback below is how the two would drift.
     *
     * Prefers the explicit link and falls back to the address: accounts created
     * before that link existed are matched by email, and any caller that
     * forgets the fallback silently reports "no customer" for them.
     */
    public static function forUser(?object $user): ?self
    {
        if ($user === null) {
            return null;
        }

        return static::query()->where('user_id', $user->id)->first()
            ?? static::query()->where('email', $user->email)->first();
    }

    /** Comparable form: spaces and punctuation are cosmetic in a VAT number. */
    public static function normaliseVatId(?string $value): string
    {
        return strtoupper(preg_replace('/[\s.-]+/', '', (string) $value) ?? '');
    }

    public function normalisedVatId(): string
    {
        return self::normaliseVatId($this->vat_id);
    }

    /**
     * True only for the value that was actually checked. Binding it to the
     * value rather than to a flag means editing the field cannot inherit the
     * old confirmation, whichever code path does the editing.
     */
    public function hasVerifiedVatId(): bool
    {
        // Both sides normalised: a verifier that stores the number in display
        // form ("DE 811 907 980") must not invalidate a genuine confirmation.
        return $this->vat_id_verified_at !== null
            && $this->normalisedVatId() !== ''
            && $this->normalisedVatId() === self::normaliseVatId($this->vat_id_verified_value);
    }

    /**
     * Has anybody actually asked this customer which they are?
     *
     * The question every other answer here is built on. NULL is not a third
     * kind of customer — it is the absence of a record, and the two methods
     * below are careful never to let it read as one.
     */
    public function hasRecordedType(): bool
    {
        return in_array($this->customer_type, self::TYPES, true);
    }

    /**
     * A business, and only if somebody wrote it down.
     *
     * Deliberately not inferred from `vat_id`. A business without a VAT number
     * is an ordinary small business, so the field is empty for a great many of
     * them, and treating "no number" as "not a business" would be wrong in the
     * one direction that matters — it would hand a business a consumer's
     * withdrawal right and, worse, would let anyone type a number to acquire
     * the opposite.
     */
    public function isBusiness(): bool
    {
        return $this->customer_type === self::TYPE_BUSINESS;
    }

    /**
     * A consumer — INCLUDING everyone nobody has asked yet.
     *
     * This is the deliberate asymmetry, and it is the whole reason the column
     * is nullable. A customer whose type was never recorded is treated as the
     * protected case: they keep the fourteen-day withdrawal right. Getting this
     * wrong costs us a refund we may not have owed; getting it the other way
     * round takes a statutory right away from somebody who has it, which is not
     * a mistake that can be made up to them afterwards.
     */
    public function isConsumer(): bool
    {
        return ! $this->isBusiness();
    }

    /** @use HasFactory<CustomerFactory> */
    use HasFactory, HasUuid;

    protected $fillable = [
        'user_id', 'name', 'contact_name', 'email', 'customer_type', 'phone', 'vat_id', 'vat_id_verified_at', 'vat_id_verified_value', 'billing_address',
        'billing_street', 'billing_postcode', 'billing_city', 'billing_country',
        'locale', 'stripe_customer_id', 'status', 'closed_at',
        'brand_display_name', 'brand_logo_path', 'brand_primary_color', 'brand_accent_color',
    ];

    protected function casts(): array
    {
        return ['closed_at' => 'datetime', 'vat_id_verified_at' => 'datetime'];
    }

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

    /**
     * Resolve branding: customer values where set, else CluPilot defaults. Used
     * for previews and snapshotted into the provisioning run so retries are
     * deterministic. NULL is stored for "unset" — defaults are never copied in.
     *
     * @return array{display_name:string,logo_path:?string,primary_color:string,accent_color:string,is_default:bool}
     */
    public function brandingResolved(): array
    {
        $defaults = (array) config('provisioning.branding_defaults');

        return [
            'display_name' => $this->brand_display_name ?: ($defaults['display_name'] ?? 'CluPilot'),
            'logo_path' => $this->brand_logo_path ?: ($defaults['logo_path'] ?? null),
            'primary_color' => $this->brand_primary_color ?: ($defaults['primary_color'] ?? '#f97316'),
            'accent_color' => $this->brand_accent_color ?: ($defaults['accent_color'] ?? '#c2560a'),
            'is_default' => $this->brand_display_name === null
                && $this->brand_logo_path === null
                && $this->brand_primary_color === null
                && $this->brand_accent_color === null,
        ];
    }

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

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

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

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

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

    /** Everything this installation has sent to them, newest first at the call site. */
    public function sentMails(): HasMany
    {
        return $this->hasMany(SentMail::class);
    }

    /**
     * True if this address already belongs to a customer identity — R21's
     * other direction, checked before an `operators` row is created or
     * renamed onto it (Admin\Settings::saveAccount(), ::inviteStaff(), and
     * the clupilot:create-operator command — one rule, three call sites).
     *
     * Checks `users` directly, not only `customers`: the two are usually in
     * lockstep (see ensureUser()), but a `users` row can outlive or predate
     * a matching `customers.email` — an email changed on one side only, or
     * legacy/orphaned data — and a `customers`-only check is blind to
     * exactly that row.
     */
    public static function emailTaken(string $email): bool
    {
        return static::query()->where('email', $email)->exists()
            || User::query()->where('email', $email)->exists();
    }

    /**
     * Find or create the portal login account for this customer and link it.
     * Race-safe against the unique email index. Refuses by construction to
     * create or link a `users` row for an address that already belongs to an
     * `operators` row (R21) — that table split killed the old "an operator
     * account never has an is_admin users row" guarantee this method used to
     * lean on, and left this one open: nothing stops the SAME address from
     * being provisioned as a customer independently of its operator row.
     *
     * @throws IdentityCollisionException when the address is already an operator's.
     */
    public function ensureUser(): User
    {
        if ($this->user) {
            return $this->user;
        }

        if (Operator::query()->where('email', $this->email)->exists()) {
            throw new IdentityCollisionException($this->email);
        }

        if (($existing = User::query()->where('email', $this->email)->first()) !== null) {
            $this->update(['user_id' => $existing->id]);

            return $existing;
        }

        try {
            $user = User::query()->create([
                'email' => $this->email,
                'name' => $this->name,
                'password' => Hash::make(Str::random(40)),
            ]);
        } catch (UniqueConstraintViolationException) {
            // Concurrent first-time creation — re-fetch and link the winner.
            $user = User::query()->where('email', $this->email)->firstOrFail();
        }

        $this->update(['user_id' => $user->id]);

        return $user->refresh();
    }
}
