<?php

namespace WPD\Security;

use WPD\Application\Config;

final class ActivationStore
{
    const MISSING = 'missing';
    const VALID = 'valid';
    const EXPIRED = 'expired';
    const INVALID = 'invalid';
    const UNAVAILABLE = 'unavailable';

    private $config;
    private $clock;
    private $path;
    private $generationPath;

    public function __construct(Config $config, Clock $clock, $varDirectory)
    {
        $this->config = $config;
        $this->clock = $clock;
        $this->path = rtrim((string) $varDirectory, '/\\') . '/activation.php';
        $this->generationPath = rtrim((string) $varDirectory, '/\\') . '/generation.php';
    }

    public function inspect($mode)
    {
        if (!is_file($this->path)) {
            return array('status' => $this->generationAllowsActivation() ? self::MISSING : self::INVALID, 'state' => null);
        }
        $state = $this->read();
        if ($state === null || !$this->generationMatchesCurrent() || !$this->verify($state) || !isset($state['mode']) || $state['mode'] !== $mode) {
            return array('status' => self::INVALID, 'state' => null);
        }
        if (!isset($state['expires_at']) || (int) $state['expires_at'] <= $this->clock->now()) {
            return array('status' => self::EXPIRED, 'state' => $state);
        }
        return array('status' => self::VALID, 'state' => $state);
    }

    public function activate($mode)
    {
        $existing = $this->inspect($mode);
        if ($existing['status'] === self::VALID) {
            return $existing['state'];
        }
        if ($existing['status'] !== self::MISSING) {
            return null;
        }
        $duration = $mode === TransportPolicy::HTTPS ? 7200 : 1800;
        $now = $this->clock->now();
        $payload = array(
            'schema' => 1,
            'activation_id' => bin2hex(random_bytes(16)),
            'mode' => $mode,
            'activated_at' => $now,
            'expires_at' => $now + $duration,
            'config_fingerprint' => $this->fingerprint(),
        );
        $payload['signature'] = $this->sign($payload);
        if (!$this->writeGeneration()) {
            return null;
        }
        if (!$this->write($payload)) {
            return null;
        }
        $stored = $this->inspect($mode);
        return $stored['status'] === self::VALID ? $stored['state'] : null;
    }

    private function read()
    {
        $contents = @file_get_contents($this->path);
        if (!is_string($contents)) {
            return null;
        }
        $json = preg_replace('/^<\?php\s+exit;\s*\?>\s*/', '', $contents);
        $state = json_decode((string) $json, true);
        return is_array($state) ? $state : null;
    }

    private function generationAllowsActivation()
    {
        if (!is_file($this->generationPath)) {
            return true;
        }
        return !$this->generationMatchesCurrent();
    }

    private function generationMatchesCurrent()
    {
        $contents = @file_get_contents($this->generationPath);
        if (!is_string($contents)) {
            return false;
        }
        $json = preg_replace('/^<\?php\s+exit;\s*\?>\s*/', '', $contents);
        $data = json_decode((string) $json, true);
        return is_array($data)
            && isset($data['schema'], $data['token_fingerprint'])
            && (int) $data['schema'] === 1
            && hash_equals(hash('sha256', (string) $this->config->get('access_token')), (string) $data['token_fingerprint']);
    }

    private function writeGeneration()
    {
        $state = array(
            'schema' => 1,
            'token_fingerprint' => hash('sha256', (string) $this->config->get('access_token')),
        );
        return $this->writeFile($this->generationPath, $state, '.generation-');
    }

    private function verify(array $state)
    {
        if (!isset($state['signature'], $state['schema'], $state['activation_id'], $state['mode'], $state['activated_at'], $state['expires_at'], $state['config_fingerprint'])) {
            return false;
        }
        if ((int) $state['schema'] !== 1 || !preg_match('/^[a-f0-9]{32}$/', (string) $state['activation_id'])) {
            return false;
        }
        if (!in_array($state['mode'], array(TransportPolicy::HTTPS, TransportPolicy::HTTP_EMERGENCY), true)) {
            return false;
        }
        if (!hash_equals($this->fingerprint(), (string) $state['config_fingerprint'])) {
            return false;
        }
        $signature = (string) $state['signature'];
        unset($state['signature']);
        return hash_equals($this->sign($state), $signature);
    }

    private function fingerprint()
    {
        $relevant = array(
            'access_token_hash' => hash('sha256', (string) $this->config->get('access_token')),
            'allowed_root' => (string) $this->config->get('allowed_root'),
            'allow_insecure_http' => (bool) $this->config->get('allow_insecure_http', false),
            'allowed_ips' => $this->config->get('allowed_ips', array()),
            'trusted_proxy_ips' => $this->config->get('trusted_proxy_ips', array()),
        );
        return hash('sha256', json_encode($relevant, JSON_UNESCAPED_SLASHES));
    }

    private function sign(array $payload)
    {
        unset($payload['signature']);
        $key = hash_hmac('sha256', 'wpd-activation-signing-key', (string) $this->config->get('access_token'), true);
        return hash_hmac('sha256', json_encode($payload, JSON_UNESCAPED_SLASHES), $key);
    }

    private function write(array $state)
    {
        return $this->writeFile($this->path, $state, '.activation-');
    }

    private function writeFile($path, array $state, $prefix)
    {
        $directory = dirname($path);
        if (!is_dir($directory) || !is_writable($directory)) {
            return false;
        }
        try {
            $temporary = $directory . '/' . $prefix . bin2hex(random_bytes(8)) . '.php';
        } catch (\Throwable $error) {
            return false;
        }
        $contents = "<?php exit; ?>\n" . json_encode($state, JSON_UNESCAPED_SLASHES);
        $written = @file_put_contents($temporary, $contents, LOCK_EX);
        if ($written === false) {
            return false;
        }
        @chmod($temporary, 0600);
        if (!@rename($temporary, $path)) {
            @unlink($temporary);
            return false;
        }
        return true;
    }
}
