<?php

declare(strict_types=1);

namespace App\Domain\Backup\Services;

use App\Domain\Account\Models\Account;
use App\Domain\Audit\Services\AuditWriter;
use App\Domain\Backup\Enums\BackupStatus;
use App\Domain\Backup\Events\BackupCreated;
use App\Domain\Backup\Events\BackupRestored;
use App\Domain\Backup\Exceptions\BackupException;
use App\Domain\Backup\Models\Backup;
use App\Domain\Sysapi\Contracts\SysapiBridge;
use App\Domain\Sysapi\Exceptions\SysapiException;
use Illuminate\Support\Facades\DB;

/**
 * Backup orchestrator — onx-backup-* script'lerini sysapi üzerinden çağırır.
 *
 * Sorumluluk:
 *   - DB satırını yazıp sysapi('backup-run')'a delege eder
 *   - Script'in döndürdüğü path/size/sha256/manifest'i kaydeder
 *   - Hata olursa status=failed + audit log
 *   - BackupCreated/Restored event'lerini fırlatır (retention/mail listener'lar dinler)
 */
final class BackupService
{
    public function __construct(private readonly SysapiBridge $sysapi) {}

    /**
     * Yeni yedek oluştur.
     *
     * @param  string  $type  full|home|databases|dns|incremental
     * @param  string  $trigger  manual|scheduled|pre-terminate
     * @param  array<string, mixed>  $options  include_mail, include_dbs, parent_backup_id, max_chain_length
     */
    public function create(
        Account $account,
        string $type = 'full',
        string $trigger = 'manual',
        array $options = [],
    ): Backup {
        // Concurrency guard — aynı hesap için zaten devam eden yedek varsa hata
        $running = Backup::where('account_id', $account->id)
            ->whereIn('status', [BackupStatus::Pending, BackupStatus::Running])
            ->first();

        if ($running) {
            throw BackupException::alreadyRunning();
        }

        // Incremental ise: parent + chain bilgilerini hazırla
        $parent = null;
        $chainId = null;
        $chainPosition = 0;
        $isChainHead = false;

        if ($type === 'incremental') {
            $parent = $this->resolveIncrementalParent($account, $options);

            if ($parent === null) {
                // Parent bulunamadıysa fallback: full backup al
                $type = 'full';
            } else {
                $chainId = $parent->chain_id;
                $chainPosition = $parent->chain_position + 1;

                // Chain max length kontrolü — limit aşılıyorsa full'a dön
                $maxChainLength = (int) ($options['max_chain_length'] ?? 14);
                if ($chainPosition >= $maxChainLength) {
                    $type = 'full';
                    $parent = null;
                    $chainId = null;
                    $chainPosition = 0;
                }
            }
        }

        // Full backup ise yeni chain başlat
        if ($type === 'full') {
            $chainId = (string) \Illuminate\Support\Str::uuid();
            $chainPosition = 0;
            $isChainHead = true;
        }

        /** @var Backup $backup */
        $backup = DB::transaction(fn () => Backup::create([
            'account_id' => $account->id,
            'parent_backup_id' => $parent?->id,
            'chain_id' => $chainId,
            'is_chain_head' => $isChainHead,
            'chain_position' => $chainPosition,
            'type' => $type,
            'trigger' => $trigger,
            'status' => BackupStatus::Running,
            'started_at' => now(),
        ]));

        $sysapiAction = $type === 'incremental' ? 'backup-run-incremental' : 'backup-run';

        try {
            $args = [
                'account_id' => $account->id,
                'username' => $account->username,
                'home_dir' => $account->homeDirectory(),
                'home' => $account->homeDirectory(),
                'type' => $type,
                'backup_id' => $backup->id,
                'chain_id' => $chainId,
                'chain_position' => $chainPosition,
                'include_mail' => $options['include_mail'] ?? true,
                'include_dbs' => $options['include_dbs'] ?? ($type === 'full' || $type === 'databases'),
                'include_dns' => $options['include_dns'] ?? ($type === 'full' || $type === 'dns'),
                'output_dir' => $options['output_dir']
                    ?? rtrim((string) config('onoxsoft.backup.directory', '/var/backups/onoxsoft'), '/')
                        .'/'.$account->username,
            ];

            // Incremental için parent path bilgisi — sysapi `--link-dest` ile kullanır
            if ($type === 'incremental' && $parent && $parent->file_path) {
                $args['parent_backup_id'] = $parent->id;
                $args['parent_path'] = $parent->file_path;
            }

            $result = $this->sysapi->run($sysapiAction, $args, timeoutSeconds: 3600);
        } catch (SysapiException $e) {
            $this->markFailed($backup, $e->getMessage());
            AuditWriter::write(
                action: 'backup.failed',
                target: $backup,
                metadata: ['error' => $e->getMessage(), 'type' => $type, 'trigger' => $trigger],
                outcome: 'failure',
                errorMessage: $e->getMessage(),
            );
            throw BackupException::sysapiFailed('create', $e->result ?? \App\Domain\Sysapi\Contracts\SysapiResult::fail(3, $e->getMessage()));
        }

        if ($result->failed()) {
            $this->markFailed($backup, $result->stderr);
            AuditWriter::write(
                action: 'backup.failed',
                target: $backup,
                metadata: array_merge($result->toAuditLog(), [
                    'type' => $type,
                    'trigger' => $trigger,
                ]),
                outcome: 'failure',
                errorMessage: $result->stderr,
            );
            throw BackupException::sysapiFailed('create', $result);
        }

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

        $backup->update([
            'status' => BackupStatus::Completed,
            'file_path' => $data['file_path'] ?? $data['path'] ?? null,
            'size_bytes' => (int) ($data['size_bytes'] ?? 0),
            'sha256' => $data['sha256'] ?? null,
            'contents' => $data['contents'] ?? $data['included'] ?? null,
            'manifest' => $data['manifest'] ?? null,
            'completed_at' => now(),
            'expires_at' => now()->addDays((int) config('onoxsoft.backup.daily_retention_days', 7)),
        ]);

        AuditWriter::write(
            action: 'backup.created',
            target: $backup,
            after: $backup->only(['type', 'trigger', 'file_path', 'size_bytes', 'status']),
            metadata: [
                'duration_seconds' => $result->durationSeconds,
                'sha256' => $data['sha256'] ?? null,
                'account_username' => $account->username,
            ],
            outcome: 'success',
        );

        $backup->refresh();

        // Retention listener bunu dinleyip eski yedekleri siler
        BackupCreated::dispatch($backup);

        return $backup;
    }

    /**
     * Yedeği geri yükle (kullanıcının onayı controller tarafında alınır).
     *
     * Incremental backup ise: chain'i baştan sona (full → ... → target) toplar ve
     * tek bir sysapi `backup-restore-chain` çağrısıyla sıralı merge eder.
     * Full backup ise: standart `backup-restore`.
     */
    public function restore(Backup $backup): void
    {
        if (! $backup->file_path) {
            throw new BackupException('Restore edilecek dosya yolu boş.');
        }

        // Incremental ise zinciri topla — full (chain head) → target
        $chainPaths = null;
        $sysapiAction = 'backup-restore';

        if ($backup->isIncremental() || ($backup->chain_id && ! $backup->isChainHead())) {
            $chain = Backup::where('chain_id', $backup->chain_id)
                ->where('chain_position', '<=', $backup->chain_position)
                ->whereNotNull('file_path')
                ->orderBy('chain_position')
                ->get();

            if ($chain->isEmpty() || $chain->first()->type !== 'full') {
                throw new BackupException(
                    "Restore başarısız: zincirin başı (full) eksik veya bozuk (chain_id: {$backup->chain_id})."
                );
            }

            $chainPaths = $chain->pluck('file_path')->all();
            $sysapiAction = 'backup-restore-chain';
        }

        try {
            $args = [
                'account_id' => $backup->account_id,
                'username' => $backup->account->username,
                'backup_id' => $backup->id,
                'backup_file' => $backup->file_path,
                'target_dir' => $backup->account->homeDirectory(),
            ];

            if ($chainPaths !== null) {
                $args['chain_paths'] = $chainPaths;
                $args['chain_id'] = $backup->chain_id;
            }

            $result = $this->sysapi->run($sysapiAction, $args, timeoutSeconds: 3600);
        } catch (SysapiException $e) {
            AuditWriter::write(
                action: 'backup.restore_failed',
                target: $backup,
                metadata: ['error' => $e->getMessage()],
                outcome: 'failure',
                errorMessage: $e->getMessage(),
            );
            throw BackupException::sysapiFailed('restore', $e->result ?? \App\Domain\Sysapi\Contracts\SysapiResult::fail(3, $e->getMessage()));
        }

        if ($result->failed()) {
            AuditWriter::write(
                action: 'backup.restore_failed',
                target: $backup,
                metadata: $result->toAuditLog(),
                outcome: 'failure',
                errorMessage: $result->stderr,
            );
            throw BackupException::sysapiFailed('restore', $result);
        }

        AuditWriter::write(
            action: 'backup.restored',
            target: $backup,
            metadata: [
                'account_username' => $backup->account->username,
                'duration_seconds' => $result->durationSeconds,
                'files_restored' => $result->data['files_restored'] ?? null,
            ],
            outcome: 'success',
        );

        BackupRestored::dispatch($backup);
    }

    /**
     * Yedeği sil (offsite kopyalar dokunulmaz — bu sadece local tarball).
     */
    public function delete(Backup $backup): void
    {
        if ($backup->file_path) {
            try {
                $result = $this->sysapi->run('backup-prune', [
                    'backup_id' => $backup->id,
                    'backup_file' => $backup->file_path,
                ]);
            } catch (SysapiException $e) {
                throw BackupException::sysapiFailed('delete', $e->result ?? \App\Domain\Sysapi\Contracts\SysapiResult::fail(3, $e->getMessage()));
            }

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

        AuditWriter::write(
            action: 'backup.deleted',
            target: $backup,
            before: $backup->only(['type', 'file_path', 'size_bytes']),
            outcome: 'success',
        );

        $backup->delete();
    }

    private function markFailed(Backup $backup, string $error): void
    {
        $backup->update([
            'status' => BackupStatus::Failed,
            'last_error' => $error,
            'completed_at' => now(),
        ]);
    }

    /**
     * Incremental backup için parent (zincirdeki bir önceki tamamlanmış backup) çöz.
     *
     * Öncelik sırası:
     *  1. options['parent_backup_id'] verilmişse onu kullan (zorla belirli zincir)
     *  2. options['chain_id'] verilmişse o chain'in en son tamamlanmış backup'ı
     *  3. Hesabın en son tamamlanmış backup'ı (full veya inc)
     *
     * Full backup zincirin başlangıcı kabul edilmediyse (eskimişse veya silindiyse)
     * null döner — caller fallback olarak full alır.
     *
     * @param  array<string, mixed>  $options
     */
    private function resolveIncrementalParent(Account $account, array $options): ?Backup
    {
        $query = Backup::where('account_id', $account->id)
            ->where('status', BackupStatus::Completed)
            ->whereNotNull('file_path')
            ->whereIn('type', ['full', 'incremental']);

        if (! empty($options['parent_backup_id'])) {
            $explicit = $query->clone()
                ->where('id', $options['parent_backup_id'])
                ->first();
            return $explicit;
        }

        if (! empty($options['chain_id'])) {
            return $query->clone()
                ->where('chain_id', $options['chain_id'])
                ->orderByDesc('chain_position')
                ->first();
        }

        // Hesabın en son tamamlanmış backup'ı — bunun chain'ine eklenir
        return $query->orderByDesc('completed_at')->first();
    }
}
