<?php

namespace App\Livewire\Admin;

use App\Models\Customer;
use App\Models\Operator;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use LivewireUI\Modal\ModalComponent;

/**
 * Correct a customer's own details.
 *
 * The console could show them and not touch them, so a moved office or a new
 * contact person meant either impersonating the customer or editing the database
 * by hand. Both are worse than a form.
 *
 * R20: a modal, not the row. And a modal is reachable WITHOUT the page's route
 * middleware, so it authorises again and reads the record itself rather than
 * trusting anything the browser hydrated.
 *
 * ## What is not here, and why
 *
 * `status` — suspending or reactivating is its own action on the customer list,
 * with its own consequences for access; a lifecycle switch hidden among address
 * fields is one somebody flips by accident.
 *
 * `stripe_customer_id` — Stripe's own identifier for them. Typing a different
 * one does not move a contract; it points our records at somebody else's
 * payments.
 *
 * The brand fields — the customer sets those for themselves in the portal, and
 * an operator overwriting somebody's logo is not a correction.
 */
class EditCustomer extends ModalComponent
{
    /** The two languages this application actually has. Empty = as configured. */
    public const LOCALES = ['de', 'en'];

    public string $uuid = '';

    public string $name = '';

    public string $contactName = '';

    public string $email = '';

    public string $phone = '';

    public string $customerType = '';

    public string $vatId = '';

    /**
     * The address, field by field — the same four the customer's own settings
     * page writes. One free-text block here and four fields there would mean an
     * operator's correction being silently reverted the next time the customer
     * saved their profile: their fields still held the old values.
     */
    public string $billingStreet = '';

    public string $billingPostcode = '';

    public string $billingCity = '';

    public string $billingCountry = '';

    public string $locale = '';

    /** The address the record had when the modal opened, to notice a change. */
    public string $originalEmail = '';

    public string $originalVatId = '';

    public function mount(string $uuid): void
    {
        $this->authorize('customers.manage');

        $customer = Customer::query()->where('uuid', $uuid)->firstOrFail();

        $this->uuid = $uuid;
        $this->name = (string) $customer->name;
        $this->contactName = (string) $customer->contact_name;
        $this->email = (string) $customer->email;
        $this->phone = (string) $customer->phone;
        $this->customerType = (string) $customer->customer_type;
        $this->vatId = (string) $customer->vat_id;
        $this->billingStreet = (string) $customer->billing_street;
        $this->billingPostcode = (string) $customer->billing_postcode;
        $this->billingCity = (string) $customer->billing_city;
        $this->billingCountry = (string) $customer->billing_country;
        $this->locale = (string) $customer->locale;
        $this->originalEmail = (string) $customer->email;
        $this->originalVatId = (string) $customer->vat_id;
    }

    public function save()
    {
        $this->authorize('customers.manage');

        // Read again, never trusted from the hydrated property: everything below
        // is decided against the STORED record.
        $customer = Customer::query()->where('uuid', $this->uuid)->firstOrFail();

        $data = $this->validate([
            'name' => 'required|string|max:255',
            'contactName' => 'nullable|string|max:255',
            'email' => [
                'required', 'email', 'max:255',
                // Among customers, ignoring this one. Two customers on one
                // address would make every lookup that matches by address —
                // inbound mail, the portal user, the register — ambiguous.
                Rule::unique('customers', 'email')->ignore($customer->id),
            ],
            'phone' => 'nullable|string|max:64',
            // Only one of the two real answers may be stored; an empty string
            // leaves the record as it was. See below for why it is never
            // cleared back to "nobody asked".
            'customerType' => ['nullable', Rule::in(Customer::TYPES)],
            'vatId' => 'nullable|string|max:64',
            'billingStreet' => 'nullable|string|max:255',
            'billingPostcode' => 'nullable|string|max:32',
            'billingCity' => 'nullable|string|max:255',
            'billingCountry' => 'nullable|string|max:255',
            'locale' => ['nullable', Rule::in(self::LOCALES)],
        ]);

        $email = strtolower(trim($data['email']));
        $emailChanged = $email !== strtolower($customer->email);

        // R21: an address belonging to an operator may never also belong to a
        // customer. Checked here rather than left to a unique index, because the
        // two identities live in different tables and no index spans them.
        if ($emailChanged && Operator::query()->where('email', $email)->exists()) {
            $this->addError('email', __('edit_customer.email_is_operator'));

            return null;
        }

        // A `users` row can outlive or predate a matching customer — see
        // Customer::emailTaken(). One belonging to somebody else must not be
        // walked into.
        if ($emailChanged && User::query()->where('email', $email)->whereKeyNot($customer->user_id ?? 0)->exists()) {
            $this->addError('email', __('edit_customer.email_taken'));

            return null;
        }

        DB::transaction(function () use ($customer, $data, $email, $emailChanged) {
            $customer->update([
                'name' => trim($data['name']),
                'contact_name' => trim((string) $data['contactName']) ?: null,
                'email' => $email,
                'phone' => trim((string) $data['phone']) ?: null,
                // Never cleared back to "unrecorded". Once somebody has
                // answered, the answer stands until it is replaced by the other
                // one — a form saved with the field blank must not quietly undo
                // a recorded consumer into an unknown, and that answer decides
                // whether a withdrawal right exists.
                'customer_type' => $data['customerType'] ?: $customer->customer_type,
                // The verification is bound to the VALUE (see
                // Customer::hasVerifiedVatId), so a new number is unverified the
                // moment it is stored. `vat_id_verified_value` is deliberately
                // left alone: it is the record of what was checked, and clearing
                // it would destroy the evidence rather than the claim.
                'vat_id' => trim((string) $data['vatId']) ?: null,
                'billing_street' => trim((string) $data['billingStreet']) ?: null,
                'billing_postcode' => trim((string) $data['billingPostcode']) ?: null,
                'billing_city' => trim((string) $data['billingCity']) ?: null,
                'billing_country' => trim((string) $data['billingCountry']) ?: null,
                // The composed block, from the same composer the portal uses —
                // one shape for one field, whichever side writes it. An address
                // that was never split keeps the block it had.
                'billing_address' => \App\Livewire\Settings::composeAddress($data) ?: $customer->billing_address,
                'locale' => $data['locale'] ?: null,
            ]);

            if (! $emailChanged || $customer->user_id === null) {
                return;
            }

            // The portal login moves with it, or the customer is locked out of
            // an account that no longer matches their record. And the new
            // address is unconfirmed by definition — nobody has shown they can
            // read it — so it goes back to unverified, which is what Laravel's
            // own profile update does for the same reason. No mail is sent from
            // here: the portal asks them to confirm on their next sign-in, and a
            // mail an operator did not knowingly send is a surprise.
            User::query()->whereKey($customer->user_id)->update([
                'email' => $email,
                'email_verified_at' => null,
            ]);
        });

        $this->dispatch('notify', message: $this->message($emailChanged, $data));

        return $this->redirectRoute('admin.customer', ['uuid' => $this->uuid], navigate: true);
    }

    /**
     * One message, and the consequence if there is one.
     *
     * Both go through the same toast and the second would replace the first, so
     * the sentence that matters has to be the only one.
     *
     * @param  array<string, mixed>  $data
     */
    private function message(bool $emailChanged, array $data): string
    {
        if ($emailChanged) {
            return __('edit_customer.saved_email_changed');
        }

        if (trim((string) $data['vatId']) !== trim($this->originalVatId)) {
            return __('edit_customer.saved_vat_unverified');
        }

        return __('edit_customer.saved');
    }

    public function render()
    {
        return view('livewire.admin.edit-customer', [
            'types' => Customer::TYPES,
            'locales' => self::LOCALES,
        ]);
    }
}
