<?php

namespace App\Livewire\Admin;

use App\Models\ExportTarget;
use App\Models\Invoice;
use App\Models\InvoiceExport;
use App\Models\InvoiceSeries;
use App\Services\Deployment\UpdateChannel;
use App\Support\CompanyProfile;
use App\Support\Settings;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithFileUploads;

/**
 * Everything an invoice needs before one can be issued.
 *
 * Its own tab rather than a section of Settings: the company's registered
 * details, the VAT rate and the Rechnungskreise are not preferences, they are
 * what appears on a legal document, and burying them under "Einstellungen"
 * beside the site-visibility switch invites somebody to change one in passing.
 *
 * None of it reaches an invoice that already exists. Every value is copied into
 * the invoice when its number is assigned, so an address corrected here is
 * correct from the next document onwards and cannot rewrite an old one.
 */
#[Layout('layouts.admin')]
class Finance extends Component
{
    use WithFileUploads;

    /** @var array<string, mixed> */
    public array $company = [];

    public float $taxRate = 20.0;

    /**
     * The one-off setup fee, in euro, as a person types it.
     *
     * Held in euro rather than cents because this is a form field, and asking
     * an operator to enter 9900 for ninety-nine euro is how a fee ends up a
     * hundred times too large. Converted on the way in and out.
     */
    public string $setupFee = '0';

    /**
     * A freshly minted collection key, shown once and never stored.
     *
     * Held in the component for the length of one page view. The private half
     * exists to be copied into a NAS and nowhere else — writing it into the
     * database "for convenience" would put a working credential in every backup
     * of that database.
     *
     * @var array<string, string>|null
     */
    public ?array $archiveKey = null;

    /** True between asking the host for a key and the host answering. */
    public bool $waitingForKey = false;

    /** New logo upload, validated on save. */
    public $logo = null;

    public string $logoPath = '';

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

        $this->company = CompanyProfile::all();
        $this->logoPath = (string) ($this->company['logo_path'] ?? '');
        $this->taxRate = CompanyProfile::taxRate();
        $this->setupFee = number_format(CompanyProfile::setupFeeCents() / 100, 2, '.', '');
    }

    public function saveCompany(): void
    {
        $this->authorize('site.manage');

        $data = $this->validate([
            'company.name' => 'required|string|max:190',
            'company.address' => 'required|string|max:190',
            'company.postcode' => 'required|string|max:20',
            'company.city' => 'required|string|max:120',
            'company.country' => 'required|string|max:120',
            'company.phone' => 'nullable|string|max:60',
            'company.email' => 'nullable|email|max:190',
            'company.website' => 'nullable|string|max:190',
            'company.register_number' => 'nullable|string|max:60',
            'company.register_court' => 'nullable|string|max:120',
            'company.vat_id' => 'required|string|max:40',
            'company.bank_name' => 'nullable|string|max:120',
            'company.iban' => 'nullable|string|max:42',
            'company.bic' => 'nullable|string|max:15',
            'company.payment_days' => 'required|integer|min:0|max:180',
            'company.payment_terms' => 'nullable|string|max:500',
            // PNG or WEBP only, and small. A PDF embeds whatever it is given,
            // and a four-megabyte photograph would be embedded in every invoice
            // ever rendered from then on.
            'logo' => 'nullable|image|mimes:png,webp|max:1024',
            'taxRate' => 'required|numeric|min:0|max:100',
            'setupFee' => 'required|numeric|min:0|max:100000',
        ]);

        if ($this->logo !== null) {
            $previous = $this->logoPath;

            $this->logoPath = $this->logo->store('company', 'public');
            $this->company['logo_path'] = $this->logoPath;

            // The old file goes only once the new one is stored. The other
            // order loses the logo entirely if the upload fails, and every
            // invoice rendered afterwards is missing it.
            if ($previous !== '' && $previous !== $this->logoPath) {
                Storage::disk('public')->delete($previous);
            }

            $this->logo = null;
        }

        CompanyProfile::put($data['company']);
        Settings::set('company.tax_rate', (float) $data['taxRate']);
        // round(), not a cast: (int) (99.95 * 100) is 9994 on a binary float,
        // and a fee one cent short of what was typed is the kind of thing
        // nobody finds until a customer does.
        Settings::set('company.setup_fee_cents', (int) round(((float) $data['setupFee']) * 100));

        $this->dispatch('notify', message: __('finance.company_saved'));
    }

    /**
     * Ask the host to mint an SSH key that may only read this archive.
     *
     * Everything an operator otherwise does by hand across three machines —
     * generate a keypair, install the public half restricted to one directory,
     * carry the private half to the NAS. None of it can happen here: the panel
     * is www-data in a container and the keys, the home directory and rrsync
     * are all on the host. So it asks, and the agent does it.
     */
    public function createPullAccess(string $uuid): void
    {
        $this->authorize('site.manage');

        $target = ExportTarget::query()->where('uuid', $uuid)->firstOrFail();

        if ($target->driver !== ExportTarget::LOCAL) {
            // Only a directory on this host has a home directory and an
            // authorized_keys to put anything in. A destination somewhere else
            // is somebody else's machine.
            $this->dispatch('notify', message: __('finance.pull_local_only'));

            return;
        }

        $accepted = app(UpdateChannel::class)->requestArchiveKey(
            (string) (auth('operator')->user()?->email ?? ''),
            $target->path,
            'clupilot-archiv-'.$target->uuid,
        );

        $this->archiveKey = null;
        $this->waitingForKey = $accepted;

        $this->dispatch('notify', message: __($accepted
            ? 'finance.pull_requested'
            : 'admin_settings.update_already_requested'));
    }

    /** Polled while waiting. Takes the key the moment the host has left one. */
    public function collectArchiveKey(): void
    {
        $this->authorize('site.manage');

        if (! $this->waitingForKey) {
            return;
        }

        $key = app(UpdateChannel::class)->takeArchiveKey();

        if ($key !== null) {
            $this->archiveKey = $key;
            $this->waitingForKey = false;
        }
    }

    public function removeLogo(): void
    {
        $this->authorize('site.manage');

        if ($this->logoPath !== '') {
            Storage::disk('public')->delete($this->logoPath);
        }

        $this->logoPath = '';
        $this->company['logo_path'] = '';
        CompanyProfile::put(['logo_path' => '']);

        $this->dispatch('notify', message: __('finance.logo_removed'));
    }

    public function render()
    {
        return view('livewire.admin.finance', [
            'series' => InvoiceSeries::query()->orderBy('kind')->get(),
            // What is still missing before an invoice may be issued at all. An
            // invoice without a name, an address or a VAT number is not a valid
            // invoice here, and issuing one is worse than refusing to.
            'missing' => CompanyProfile::missingForInvoicing(),
            // Counted from the join table, never by asking the destinations.
            // A stat() on a network mount that has gone away blocks for the
            // mount's whole timeout, and this is a page an operator opens when
            // something is already wrong.
            'targets' => ExportTarget::query()->orderBy('name')->get()->map(fn (ExportTarget $t) => [
                'model' => $t,
                'pending' => Invoice::query()->whereDoesntHave('exports', fn ($q) => $q
                    ->where('export_target_id', $t->id)->whereNotNull('exported_at'))->count(),
                'failed' => InvoiceExport::query()->where('export_target_id', $t->id)
                    ->whereNotNull('error')->whereNull('exported_at')->count(),
            ]),
        ]);
    }
}
