<?php

declare(strict_types=1);

namespace App\Domain\Security\Gpg\Services;

use App\Domain\Account\Models\Account;
use App\Domain\Security\Gpg\Exceptions\GpgKeyException;
use App\Domain\Sysapi\Contracts\SysapiBridge;
use Illuminate\Support\Facades\DB;

/**
 * GnuPG self-service provisioner.
 *
 * Real OS work is delegated to the onx-gpg-key-* bash wrappers via sysapi.
 * This service is responsible for:
 *   - Validating inputs (key size, expires, fingerprint format)
 *   - Calling the correct onx-gpg-* action
 *   - Keeping the gpg_keys DB table in sync with what's in /home/<user>/.gnupg
 *
 * Idempotency: the bash side detects when a key with the same UID already
 * exists and returns `created:false` instead of duplicating it.
 */
final class GpgKeyProvisioner
{
    public function __construct(private readonly SysapiBridge $sysapi) {}

    /**
     * Generate a new key pair in the user's keyring.
     *
     * @param  array{name:string, email:string, comment?:?string, key_size?:int, expires_in_days?:?int, passphrase?:string}  $params
     * @return array{id:int, fingerprint:string, key_id:string, public_key:string, created:bool}
     *
     * @throws GpgKeyException
     */
    public function generate(Account $account, array $params): array
    {
        $name = trim((string) ($params['name'] ?? ''));
        $email = trim((string) ($params['email'] ?? ''));
        $keySize = (int) ($params['key_size'] ?? 2048);
        $comment = (string) ($params['comment'] ?? '');
        $passphrase = (string) ($params['passphrase'] ?? '');
        $expiresDays = $params['expires_in_days'] ?? null;

        if ($name === '' || $email === '') {
            throw GpgKeyException::invalidKey('isim ve email zorunlu');
        }
        if (! in_array($keySize, [2048, 3072, 4096], true)) {
            throw GpgKeyException::invalidKey('key_size 2048/3072/4096 olmalı');
        }

        $expires = $expiresDays === null || $expiresDays === ''
            ? '0'
            : ((int) $expiresDays).'d';

        $result = $this->sysapi->run('gpg-key-gen', [
            'username' => $account->username,
            'real_name' => $name,
            'email' => $email,
            'comment' => $comment,
            'key_size' => $keySize,
            'expires' => $expires,
            'passphrase' => $passphrase,
        ], timeoutSeconds: 180);

        if ($result->failed()) {
            throw GpgKeyException::sysapiFailed('generate', $result);
        }

        $data = $result->data ?? [];
        $fingerprint = strtoupper((string) ($data['fingerprint'] ?? ''));
        $publicKey = (string) ($data['public_key'] ?? '');

        if ($fingerprint === '' || $publicKey === '') {
            throw GpgKeyException::sysapiFailed('generate', $result);
        }

        $keyId = substr($fingerprint, -16);
        $expiresAt = $expiresDays !== null && $expiresDays !== ''
            ? now()->addDays((int) $expiresDays)
            : null;

        // Upsert: idempotent → same fingerprint reuses existing row
        $existing = DB::table('gpg_keys')
            ->where('account_id', $account->id)
            ->where('fingerprint', $fingerprint)
            ->first();

        if ($existing !== null) {
            DB::table('gpg_keys')->where('id', $existing->id)->update([
                'public_key' => $publicKey,
                'updated_at' => now(),
            ]);
            $id = (int) $existing->id;
        } else {
            $id = (int) DB::table('gpg_keys')->insertGetId([
                'account_id' => $account->id,
                'name' => $name,
                'email' => $email,
                'comment' => $comment !== '' ? $comment : null,
                'key_size' => $keySize,
                'expires_in_days' => $expiresDays !== null && $expiresDays !== '' ? (int) $expiresDays : null,
                'fingerprint' => $fingerprint,
                'key_id' => $keyId,
                'public_key' => $publicKey,
                'has_private' => true,
                'expires_at' => $expiresAt,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }

        return [
            'id' => $id,
            'fingerprint' => $fingerprint,
            'key_id' => $keyId,
            'public_key' => $publicKey,
            'created' => (bool) ($data['created'] ?? true),
        ];
    }

    /**
     * Import an armored public key into the user's keyring.
     *
     * @return array{id:int, fingerprint:string, key_id:string, imported:bool}
     *
     * @throws GpgKeyException
     */
    public function importKey(Account $account, string $name, string $email, string $armored): array
    {
        $armored = trim($armored);
        if (! str_contains($armored, 'BEGIN PGP')) {
            throw GpgKeyException::invalidKey('armored block bulunamadı');
        }

        $result = $this->sysapi->run('gpg-key-import', [
            'username' => $account->username,
            'public_key' => $armored,
        ]);

        if ($result->failed()) {
            throw GpgKeyException::sysapiFailed('import', $result);
        }

        $data = $result->data ?? [];
        $fingerprint = strtoupper((string) ($data['fingerprint'] ?? ''));
        if ($fingerprint === '') {
            throw GpgKeyException::sysapiFailed('import', $result);
        }
        $keyId = substr($fingerprint, -16);

        $existing = DB::table('gpg_keys')
            ->where('account_id', $account->id)
            ->where('fingerprint', $fingerprint)
            ->first();

        if ($existing !== null) {
            DB::table('gpg_keys')->where('id', $existing->id)->update([
                'public_key' => $armored,
                'updated_at' => now(),
            ]);
            $id = (int) $existing->id;
        } else {
            $id = (int) DB::table('gpg_keys')->insertGetId([
                'account_id' => $account->id,
                'name' => $name,
                'email' => $email,
                'comment' => null,
                'key_size' => 0,
                'expires_in_days' => null,
                'fingerprint' => $fingerprint,
                'key_id' => $keyId,
                'public_key' => $armored,
                'has_private' => false,
                'expires_at' => null,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }

        return [
            'id' => $id,
            'fingerprint' => $fingerprint,
            'key_id' => $keyId,
            'imported' => (bool) ($data['imported'] ?? true),
        ];
    }

    /**
     * Pull the live key list straight from /home/<user>/.gnupg.
     *
     * @return array{username:string, count:int, keys:array<int, array<string, mixed>>}
     */
    public function listKeys(Account $account): array
    {
        $result = $this->sysapi->run('gpg-key-list', [
            'username' => $account->username,
        ]);

        if ($result->failed()) {
            throw GpgKeyException::sysapiFailed('list', $result);
        }

        return $result->data ?? [
            'username' => $account->username,
            'count' => 0,
            'keys' => [],
        ];
    }

    /**
     * Delete a key by fingerprint. Drops both DB row and on-disk keyring entry.
     *
     * @return array{fingerprint:string, deleted:bool}
     *
     * @throws GpgKeyException
     */
    public function deleteKey(Account $account, string $fingerprint): array
    {
        $fingerprint = strtoupper(preg_replace('/\s+/', '', $fingerprint) ?? '');
        if (! preg_match('/^[A-F0-9]{40}$/', $fingerprint)) {
            throw GpgKeyException::invalidKey('40-hex fingerprint bekleniyor');
        }

        $result = $this->sysapi->run('gpg-key-delete', [
            'username' => $account->username,
            'fingerprint' => $fingerprint,
        ]);

        if ($result->failed()) {
            throw GpgKeyException::sysapiFailed('delete', $result);
        }

        DB::table('gpg_keys')
            ->where('account_id', $account->id)
            ->where('fingerprint', $fingerprint)
            ->delete();

        return [
            'fingerprint' => $fingerprint,
            'deleted' => (bool) (($result->data['deleted'] ?? null) ?? true),
        ];
    }

    /**
     * Export armored key body (public by default).
     *
     * @return array{fingerprint:string, armor:string, kind:string}
     *
     * @throws GpgKeyException
     */
    public function exportPublic(Account $account, string $fingerprint, string $kind = 'public'): array
    {
        $fingerprint = strtoupper(preg_replace('/\s+/', '', $fingerprint) ?? '');
        if (! preg_match('/^[A-F0-9]{40}$/', $fingerprint)) {
            throw GpgKeyException::invalidKey('40-hex fingerprint bekleniyor');
        }
        if (! in_array($kind, ['public', 'secret'], true)) {
            throw GpgKeyException::invalidKey("kind must be public|secret, got '{$kind}'");
        }

        $result = $this->sysapi->run('gpg-key-export', [
            'username' => $account->username,
            'fingerprint' => $fingerprint,
            'kind' => $kind,
        ]);

        if ($result->failed()) {
            throw GpgKeyException::sysapiFailed('export', $result);
        }

        $data = $result->data ?? [];

        return [
            'fingerprint' => $fingerprint,
            'armor' => (string) ($data['armor'] ?? ''),
            'kind' => $kind,
        ];
    }
}
