<?php

namespace Acms\Services\Database;

use Acms\Services\Facades\LocalStorage;

/**
 * バックアップのリストア進行状況を、DB に依存しないローカルファイルで管理する。
 *
 * リストア中は DB のテーブルが入れ替わり一時的に利用できない状態になるため、
 * ブートストラップの DB 接続前に「復元中かどうか」を判定できる必要がある。
 * そのため WordPress の `.maintenance` ファイルと同様に、状態をローカルファイルへ書き出す。
 * 保存先は他のロック／進捗ファイル（`Lock` や DB エクスポートの進捗 JSON）と揃えて CACHE_DIR 配下とする。
 */
class RestoreStatus
{
    /** 復元処理が進行中 */
    public const STATE_RUNNING = 'running';

    /** 復元処理が失敗して終了した */
    public const STATE_FAILED = 'failed';

    /** 復元処理が行われていない（または完了済み） */
    public const STATE_NONE = 'none';

    /**
     * `running` 状態をこの秒数を超えて保持していた場合、異常終了とみなして
     * ロックを解除する。復元処理がクラッシュしてもサイトが永久にロックされるのを防ぐ。
     */
    public const DEFAULT_TTL = 1800;

    /**
     * 状態ファイルのパス
     *
     * @var string
     */
    private $path;

    /**
     * `running` を有効とみなす最大秒数
     *
     * @var int
     */
    private $ttl;

    /**
     * @param string|null $path 状態ファイルのパス（省略時は既定パス）
     * @param int $ttl `running` を有効とみなす最大秒数
     */
    public function __construct(?string $path = null, int $ttl = self::DEFAULT_TTL)
    {
        $this->path = $path ?? self::defaultPath();
        $this->ttl = $ttl;
    }

    /**
     * 状態ファイルの既定パス（CACHE_DIR 配下。Web 非公開）。
     *
     * @return string
     */
    public static function defaultPath(): string
    {
        $dir = defined('CACHE_DIR') ? CACHE_DIR : 'cache/';
        return $dir . 'restore-status.json';
    }

    /**
     * 復元開始を記録する。
     *
     * @param int|null $now 現在時刻（テスト用に注入可能）
     * @param string $token 操作者識別用トークン（Cookie と照合する）
     * @return void
     */
    public function begin(?int $now = null, string $token = ''): void
    {
        $this->write([
            'state' => self::STATE_RUNNING,
            'started_at' => $now ?? time(),
            'token' => $token,
            'message' => '',
        ]);
    }

    /**
     * 復元失敗を記録する。開始時のトークン・開始時刻は引き継ぐ。
     *
     * @param string $message 利用者へ示すメッセージ
     * @param int|null $now 現在時刻（テスト用に注入可能）
     * @return void
     */
    public function fail(string $message = '', ?int $now = null): void
    {
        $current = $now ?? time();
        $existing = $this->read();
        $startedAt = (is_array($existing) && isset($existing['started_at'])) ? (int)$existing['started_at'] : $current;
        $token = (is_array($existing) && isset($existing['token']) && is_string($existing['token'])) ? $existing['token'] : '';
        $this->write([
            'state' => self::STATE_FAILED,
            'started_at' => $startedAt,
            'failed_at' => $current,
            'token' => $token,
            'message' => $message,
        ]);
    }

    /**
     * 復元を開始した操作者かどうかを、保存済みトークンと照合して判定する。
     * 復元中は DB が利用できずセッション認証が使えないため、DB 非依存のトークンで識別する。
     *
     * @param string|null $token Cookie 等から受け取ったトークン
     * @return bool
     */
    public function verifyToken(?string $token): bool
    {
        if ($token === null || $token === '') {
            return false;
        }
        $data = $this->read();
        if ($data === null || !isset($data['token']) || !is_string($data['token']) || $data['token'] === '') {
            return false;
        }
        return hash_equals($data['token'], $token);
    }

    /**
     * 復元完了として状態ファイルを削除する。
     *
     * @return void
     */
    public function finish(): void
    {
        if (LocalStorage::exists($this->path)) {
            LocalStorage::remove($this->path);
        }
    }

    /**
     * 状態ファイルの内容を返す。存在しない・読めない・壊れている場合は null。
     *
     * @return array<string, mixed>|null
     */
    public function read(): ?array
    {
        if (!LocalStorage::exists($this->path)) {
            return null;
        }
        try {
            $raw = LocalStorage::get($this->path, dirname($this->path));
        } catch (\Throwable $e) {
            return null;
        }
        if (!is_string($raw)) {
            return null;
        }
        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return null;
        }
        return $data;
    }

    /**
     * 復元処理が進行中でサイトをロックすべきかを返す。
     * `running` かつ TTL 内のときだけ true。失効した running や failed は false。
     *
     * @param int|null $now 現在時刻（テスト用に注入可能）
     * @return bool
     */
    public function isActive(?int $now = null): bool
    {
        $data = $this->read();
        if ($data === null) {
            return false;
        }
        $state = isset($data['state']) ? (string)$data['state'] : '';
        if ($state !== self::STATE_RUNNING) {
            return false;
        }
        $startedAt = isset($data['started_at']) ? (int)$data['started_at'] : 0;
        $current = $now ?? time();
        return ($current - $startedAt) <= $this->ttl;
    }

    /**
     * ステータス API（進捗ページのポーリング）用の配列を返す。
     *
     * @param int|null $now 現在時刻（テスト用に注入可能）
     * @return array{state: string, elapsed: int, message: string}
     */
    public function toResponse(?int $now = null): array
    {
        $data = $this->read();
        $current = $now ?? time();
        if ($data === null) {
            return ['state' => self::STATE_NONE, 'elapsed' => 0, 'message' => ''];
        }
        $state = isset($data['state']) ? (string)$data['state'] : self::STATE_NONE;
        if ($state === self::STATE_RUNNING && !$this->isActive($now)) {
            // TTL 切れの running は異常終了として扱う
            $state = self::STATE_FAILED;
        }
        $startedAt = isset($data['started_at']) ? (int)$data['started_at'] : $current;
        $message = isset($data['message']) ? (string)$data['message'] : '';
        return [
            'state' => $state,
            'elapsed' => max(0, $current - $startedAt),
            'message' => $message,
        ];
    }

    /**
     * 状態ファイルを書き出す。書き込みは安全装置のため、失敗してもリストア処理は継続させる。
     *
     * @param array<string, mixed> $data
     * @return void
     */
    private function write(array $data): void
    {
        $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            return;
        }
        try {
            $dir = dirname($this->path);
            if (!LocalStorage::exists($dir)) {
                LocalStorage::makeDirectory($dir);
            }
            LocalStorage::put($this->path, $json);
        } catch (\Throwable $e) {
            // 状態ファイルの書き込み失敗は致命的ではないため握りつぶす
        }
    }
}
