<?php

declare(strict_types=1);

namespace App\Domain\Security\Fail2ban\Services;

use App\Domain\Audit\Services\AuditWriter;
use App\Domain\Sysapi\Contracts\SysapiBridge;
use App\Domain\Sysapi\Exceptions\SysapiException;
use RuntimeException;

/**
 * fail2ban provisioner — manages /etc/fail2ban/jail.d/<jail>.local files,
 * manual ban/unban actions, and status reads.
 *
 * sysapi commands used:
 *   - fail2ban-jail-write
 *   - fail2ban-ban
 *   - fail2ban-unban
 *   - fail2ban-status
 */
final class Fail2banProvisioner
{
    public function __construct(private readonly SysapiBridge $sysapi) {}

    /**
     * Write a jail file under /etc/fail2ban/jail.d and reload fail2ban.
     *
     * @param  array{jail_name:string,enabled?:bool,port?:string,filter?:string,logpath?:string,maxretry?:int,findtime?:int,bantime?:int,action?:string}  $payload
     * @return array<string, mixed>
     */
    public function writeJail(array $payload): array
    {
        $jail = (string) ($payload['jail_name'] ?? '');
        if ($jail === '') {
            throw new RuntimeException('jail_name zorunludur.');
        }

        $sysPayload = [
            'jail_name' => $jail,
            'enabled' => (bool) ($payload['enabled'] ?? true),
            'port' => (string) ($payload['port'] ?? 'ssh,22'),
            'filter' => (string) ($payload['filter'] ?? $jail),
            'logpath' => (string) ($payload['logpath'] ?? '/var/log/secure'),
            'maxretry' => (int) ($payload['maxretry'] ?? 5),
            'findtime' => (int) ($payload['findtime'] ?? 600),
            'bantime' => (int) ($payload['bantime'] ?? 3600),
            'action' => (string) ($payload['action'] ?? '%(action_)s'),
        ];

        try {
            $result = $this->sysapi->run('fail2ban-jail-write', $sysPayload, timeoutSeconds: 30);
        } catch (SysapiException $e) {
            AuditWriter::write(
                action: 'fail2ban.jail_write_failed',
                metadata: array_merge($sysPayload, ['error' => $e->getMessage()]),
                outcome: 'failure',
                errorMessage: $e->getMessage(),
            );
            throw new RuntimeException("Fail2ban jail yazılamadı: {$e->getMessage()}", previous: $e);
        }
        if ($result->failed()) {
            throw new RuntimeException("Fail2ban jail yazılamadı: {$result->stderr}");
        }

        AuditWriter::write(
            action: 'fail2ban.jail_written',
            metadata: array_merge($sysPayload, $result->data ?? []),
            outcome: 'success',
        );

        return $result->data ?? [];
    }

    public function banIp(string $jail, string $ip): bool
    {
        try {
            $result = $this->sysapi->run('fail2ban-ban', [
                'jail' => $jail,
                'ip' => $ip,
            ], timeoutSeconds: 15);
        } catch (SysapiException $e) {
            throw new RuntimeException("IP banlanamadı: {$e->getMessage()}", previous: $e);
        }
        if ($result->failed()) {
            throw new RuntimeException("IP banlanamadı: {$result->stderr}");
        }

        AuditWriter::write(
            action: 'fail2ban.banned',
            metadata: ['jail' => $jail, 'ip' => $ip],
            outcome: 'success',
        );

        return (bool) ($result->data['banned'] ?? true);
    }

    public function unbanIp(string $jail, string $ip): bool
    {
        try {
            $result = $this->sysapi->run('fail2ban-unban', [
                'jail' => $jail,
                'ip' => $ip,
            ], timeoutSeconds: 15);
        } catch (SysapiException $e) {
            throw new RuntimeException("IP unban edilemedi: {$e->getMessage()}", previous: $e);
        }
        if ($result->failed()) {
            throw new RuntimeException("IP unban edilemedi: {$result->stderr}");
        }

        AuditWriter::write(
            action: 'fail2ban.unbanned',
            metadata: ['jail' => $jail, 'ip' => $ip],
            outcome: 'success',
        );

        return (bool) ($result->data['unbanned'] ?? true);
    }

    /**
     * Status of all jails (no arg) or a single jail.
     *
     * @return array<string, mixed>
     */
    public function getStatus(?string $jail = null): array
    {
        $args = $jail ? ['jail' => $jail] : [];

        try {
            $result = $this->sysapi->run('fail2ban-status', $args, timeoutSeconds: 10);
        } catch (SysapiException $e) {
            throw new RuntimeException("Fail2ban durumu okunamadı: {$e->getMessage()}", previous: $e);
        }
        if ($result->failed()) {
            throw new RuntimeException("Fail2ban durumu okunamadı: {$result->stderr}");
        }

        return $result->data ?? [];
    }
}
