<?php
/**
 * Local, side-effect-free connection state snapshot.
 *
 * @package CalliopeSocialPublisher
 */

if (!defined('ABSPATH')) {
    exit;
}

final class Calliope_SP_Connection_State {
    const SCHEMA = 'connection-state-v1';

    const MAX_MESSAGE_LENGTH = 300;
    const MAX_ACTION_LENGTH  = 120;

    /** @var array<string,mixed> */
    private $data = array();

    /** @var array<string,bool> */
    private static $allowed_statuses = array(
        'not_configured'      => true,
        'credentials_saved'   => true,
        'connected_unverified'=> true,
        'identity_missing'    => true,
        'target_missing'      => true,
        'permission_missing'  => true,
        'environment_mismatch'=> true,
        'publish_access_denied'=> true,
        'temporary_error'     => true,
        'refresh_required'    => true,
        'token_expired'       => true,
        'token_invalid'       => true,
        'ready_to_publish'    => true,
        'configuration_error' => true,
    );

    /** @var array<string,bool> */
    private static $allowed_tones = array(
        'success' => true,
        'warning' => true,
        'error'   => true,
        'info'    => true,
        'neutral' => true,
    );

    /** @var array<string,bool> */
    private static $allowed_health = array(
        'unknown'  => true,
        'healthy'  => true,
        'degraded' => true,
        'invalid'  => true,
    );

    /** @var array<string,int> */
    private static $evidence_ranks = array(
        'E0' => 0,
        'E1' => 1,
        'E2' => 2,
        'E3' => 3,
        'E4' => 4,
        'E5' => 5,
        'E6' => 6,
    );

    /** @var array<string,bool> */
    private static $allowed_connection_methods = array(
        'oauth1'          => true,
        'oauth2'          => true,
        'token'           => true,
        'app_password'    => true,
        'instance_oauth2' => true,
        'bot_token'       => true,
        'unknown'         => true,
    );

    /** @var array<string,string> */
    private static $default_tones = array(
        'ready_to_publish'     => 'success',
        'credentials_saved'    => 'warning',
        'connected_unverified' => 'warning',
        'identity_missing'     => 'warning',
        'target_missing'       => 'warning',
        'refresh_required'     => 'warning',
        'permission_missing'   => 'error',
        'environment_mismatch' => 'error',
        'publish_access_denied'=> 'error',
        'temporary_error'      => 'warning',
        'token_expired'        => 'error',
        'token_invalid'        => 'error',
        'not_configured'       => 'error',
        'configuration_error'  => 'error',
    );

    /**
     * Constructor is private; use create() or from_array().
     *
     * @param array<string,mixed> $data Normalized state.
     */
    private function __construct(array $data) {
        $this->data = $data;
    }

    /**
     * Create a validated connection state.
     *
     * @param array<string,mixed> $data             Candidate public fields.
     * @param string              $minimum_evidence Minimum evidence needed for readiness.
     * @return self|WP_Error
     */
    public static function create(array $data, $minimum_evidence = 'E0') {
        return self::build($data, $minimum_evidence);
    }

    /**
     * Rebuild a validated connection state from a serialized array.
     *
     * @param array<string,mixed> $data             Serialized state.
     * @param string              $minimum_evidence Minimum evidence needed for readiness.
     * @return self|WP_Error
     */
    public static function from_array(array $data, $minimum_evidence = 'E0') {
        if (isset($data['schema']) && self::SCHEMA !== $data['schema']) {
            return new WP_Error('calliope_sp_connection_state_schema', 'Unknown connection state schema.');
        }

        return self::build($data, $minimum_evidence);
    }

    /**
     * Serialize only the public, non-sensitive contract fields.
     *
     * @return array<string,mixed>
     */
    public function to_array() {
        return array(
            'schema'            => $this->data['schema'],
            'network'           => $this->data['network'],
            'status'            => $this->data['status'],
            'tone'              => $this->data['tone'],
            'health'            => $this->data['health'],
            'is_configured'     => $this->data['is_configured'],
            'is_authenticated'  => $this->data['is_authenticated'],
            'is_identity_known' => $this->data['is_identity_known'],
            'has_target'        => $this->data['has_target'],
            'has_permission'    => $this->data['has_permission'],
            'is_ready'          => $this->data['is_ready'],
            'needs_attention'   => $this->data['needs_attention'],
            'evidence_level'    => $this->data['evidence_level'],
            'message'           => $this->data['message'],
            'action'            => $this->data['action'],
            'expires_at'        => $this->data['expires_at'],
            'validated_at'      => $this->data['validated_at'],
            'connection_method' => $this->data['connection_method'],
        );
    }

    /** @return string */
    public function get_status() {
        return $this->data['status'];
    }

    /** @return string */
    public function get_network() {
        return $this->data['network'];
    }

    /** @return string */
    public function get_health() {
        return $this->data['health'];
    }

    /** @return string */
    public function get_evidence_level() {
        return $this->data['evidence_level'];
    }

    /** @return bool */
    public function is_ready() {
        return $this->data['is_ready'];
    }

    /** @return bool */
    public function needs_attention() {
        return $this->data['needs_attention'];
    }

    /**
     * Read a public field without exposing mutable internal data.
     *
     * @param string $key     Public field key.
     * @param mixed  $default Default value.
     * @return mixed
     */
    public function get($key, $default = null) {
        return array_key_exists($key, $this->data) ? $this->data[$key] : $default;
    }

    /**
     * Build and validate normalized data.
     *
     * @param array<string,mixed> $input            Input fields.
     * @param string              $minimum_evidence Minimum evidence needed for readiness.
     * @return self|WP_Error
     */
    private static function build(array $input, $minimum_evidence) {
        $minimum_evidence = self::normalize_evidence($minimum_evidence);
        if (is_wp_error($minimum_evidence)) {
            return $minimum_evidence;
        }

        if (isset($input['schema']) && self::SCHEMA !== $input['schema']) {
            return new WP_Error('calliope_sp_connection_state_schema', 'Unknown connection state schema.');
        }

        $network = self::normalize_network(isset($input['network']) ? $input['network'] : '');
        if (is_wp_error($network)) {
            return $network;
        }

        $status = self::normalize_allowlisted(
            isset($input['status']) ? $input['status'] : 'not_configured',
            self::$allowed_statuses,
            'status'
        );
        if (is_wp_error($status)) {
            return $status;
        }

        $health_default = self::default_health_for_status($status);
        $health = self::normalize_allowlisted(
            isset($input['health']) ? $input['health'] : $health_default,
            self::$allowed_health,
            'health'
        );
        if (is_wp_error($health)) {
            return $health;
        }

        $tone_default = self::$default_tones[$status];
        if ('ready_to_publish' === $status && 'degraded' === $health) {
            $tone_default = 'warning';
        }
        $tone = self::normalize_allowlisted(
            isset($input['tone']) ? $input['tone'] : $tone_default,
            self::$allowed_tones,
            'tone'
        );
        if (is_wp_error($tone)) {
            return $tone;
        }

        $evidence = self::normalize_evidence(isset($input['evidence_level']) ? $input['evidence_level'] : 'E0');
        if (is_wp_error($evidence)) {
            return $evidence;
        }

        $method = self::normalize_allowlisted(
            isset($input['connection_method']) ? $input['connection_method'] : 'unknown',
            self::$allowed_connection_methods,
            'connection_method'
        );
        if (is_wp_error($method)) {
            return $method;
        }

        $boolean_fields = array(
            'is_configured',
            'is_authenticated',
            'is_identity_known',
            'has_target',
            'has_permission',
        );
        $booleans = array();
        foreach ($boolean_fields as $field) {
            $value = array_key_exists($field, $input) ? $input[$field] : false;
            $value = self::normalize_boolean($value, $field);
            if (is_wp_error($value)) {
                return $value;
            }
            $booleans[$field] = $value;
        }

        $expires_at = self::normalize_timestamp(isset($input['expires_at']) ? $input['expires_at'] : 0, 'expires_at');
        if (is_wp_error($expires_at)) {
            return $expires_at;
        }
        $validated_at = self::normalize_timestamp(isset($input['validated_at']) ? $input['validated_at'] : 0, 'validated_at');
        if (is_wp_error($validated_at)) {
            return $validated_at;
        }

        $message = self::normalize_text(isset($input['message']) ? $input['message'] : '', self::MAX_MESSAGE_LENGTH, 'message');
        if (is_wp_error($message)) {
            return $message;
        }
        $action = self::normalize_text(isset($input['action']) ? $input['action'] : '', self::MAX_ACTION_LENGTH, 'action');
        if (is_wp_error($action)) {
            return $action;
        }

        $coherence = self::validate_coherence(
            $status,
            $health,
            $evidence,
            $minimum_evidence,
            $booleans,
            $validated_at
        );
        if (is_wp_error($coherence)) {
            return $coherence;
        }

        $is_ready = self::derive_readiness(
            $status,
            $health,
            $evidence,
            $minimum_evidence,
            $booleans
        );

        $needs_attention = self::derive_needs_attention($status, $health, $is_ready, $action);

        return new self(array(
            'schema'            => self::SCHEMA,
            'network'           => $network,
            'status'            => $status,
            'tone'              => $tone,
            'health'            => $health,
            'is_configured'     => $booleans['is_configured'],
            'is_authenticated'  => $booleans['is_authenticated'],
            'is_identity_known' => $booleans['is_identity_known'],
            'has_target'        => $booleans['has_target'],
            'has_permission'    => $booleans['has_permission'],
            'is_ready'          => $is_ready,
            'needs_attention'   => $needs_attention,
            'evidence_level'    => $evidence,
            'message'           => $message,
            'action'            => $action,
            'expires_at'        => $expires_at,
            'validated_at'      => $validated_at,
            'connection_method' => $method,
        ));
    }

    /**
     * Validate structural coherence.
     *
     * @param string              $status           Readiness status.
     * @param string              $health           Health status.
     * @param string              $evidence         Evidence level.
     * @param string              $minimum_evidence Minimum evidence for readiness.
     * @param array<string,bool>  $flags            Evidence flags.
     * @param int                 $validated_at      Validation timestamp.
     * @return true|WP_Error
     */
    private static function validate_coherence($status, $health, $evidence, $minimum_evidence, array $flags, $validated_at) {
        $rank = self::$evidence_ranks[$evidence];

        if (0 === $rank && $flags['is_configured']) {
            return self::coherence_error('E0 cannot be configured.');
        }
        if ($rank >= 1 && !$flags['is_configured']) {
            return self::coherence_error('E1 or higher requires saved configuration.');
        }
        if (1 === $rank && $flags['is_authenticated']) {
            return self::coherence_error('E1 cannot be authenticated.');
        }
        if ($rank >= 2 && !$flags['is_authenticated']) {
            return self::coherence_error('E2 or higher requires authentication.');
        }
        if ($rank >= 3 && !$flags['is_identity_known']) {
            return self::coherence_error('E3 or higher requires a known identity.');
        }
        if ($rank >= 4 && !$flags['has_target']) {
            return self::coherence_error('E4 or higher requires a target.');
        }
        if ($rank >= 5 && !$flags['has_permission']) {
            return self::coherence_error('E5 or higher requires publishing permission.');
        }
        if (6 === $rank && (!$flags['is_configured'] || !$flags['is_authenticated'] || !$flags['is_identity_known'] || !$flags['has_target'] || !$flags['has_permission'] || $validated_at <= 0)) {
            return self::coherence_error('E6 requires complete validated evidence.');
        }

        if ('not_configured' === $status && ($flags['is_configured'] || $flags['is_authenticated'])) {
            return self::coherence_error('A not configured state cannot be configured or authenticated.');
        }
        if ('credentials_saved' === $status && (!$flags['is_configured'] || $flags['is_authenticated'])) {
            return self::coherence_error('Credentials saved requires configuration without authentication.');
        }
        if ('connected_unverified' === $status && (!$flags['is_configured'] || !$flags['is_authenticated'])) {
            return self::coherence_error('Connected unverified requires an authenticated configuration.');
        }
        if ('identity_missing' === $status && (!$flags['is_configured'] || !$flags['is_authenticated'] || $flags['is_identity_known'])) {
            return self::coherence_error('Identity missing requires authentication without a known identity.');
        }
        if ('target_missing' === $status && (!$flags['is_configured'] || !$flags['is_authenticated'] || !$flags['is_identity_known'] || $flags['has_target'])) {
            return self::coherence_error('Target missing requires a known identity without a target.');
        }
        if (in_array($status, array('permission_missing', 'environment_mismatch', 'publish_access_denied', 'temporary_error'), true) && (!$flags['is_configured'] || !$flags['is_authenticated'] || $flags['has_permission'])) {
            return self::coherence_error('This connection state requires authentication without confirmed publishing permission.');
        }

        if (in_array($status, array('token_invalid', 'token_expired', 'configuration_error', 'environment_mismatch', 'publish_access_denied'), true) && 'invalid' !== $health) {
            return self::coherence_error('The selected status requires invalid health.');
        }

        if ('ready_to_publish' === $status) {
            if ('invalid' === $health) {
                return self::coherence_error('A ready connection cannot have invalid health.');
            }
            if (!in_array($health, array('healthy', 'degraded'), true)) {
                return self::coherence_error('A ready connection requires healthy or degraded health.');
            }
            if (!$flags['is_configured'] || !$flags['is_authenticated'] || !$flags['is_identity_known'] || !$flags['has_target'] || !$flags['has_permission']) {
                return self::coherence_error('A ready connection requires complete local evidence.');
            }
            if (self::$evidence_ranks[$evidence] < self::$evidence_ranks[$minimum_evidence]) {
                return self::coherence_error('The evidence level is below the minimum required for readiness.');
            }
        }

        return true;
    }

    /**
     * Derive readiness rather than trusting caller input.
     *
     * @param string             $status           Status.
     * @param string             $health           Health.
     * @param string             $evidence         Evidence.
     * @param string             $minimum_evidence Minimum evidence.
     * @param array<string,bool> $flags            Evidence flags.
     * @return bool
     */
    private static function derive_readiness($status, $health, $evidence, $minimum_evidence, array $flags) {
        return 'ready_to_publish' === $status
            && 'invalid' !== $health
            && $flags['is_configured']
            && $flags['is_authenticated']
            && $flags['is_identity_known']
            && $flags['has_target']
            && $flags['has_permission']
            && self::$evidence_ranks[$evidence] >= self::$evidence_ranks[$minimum_evidence];
    }

    /**
     * Derive attention state independently from readiness.
     *
     * @param string $status   Status.
     * @param string $health   Health.
     * @param bool   $is_ready Readiness.
     * @param string $action   Suggested action key/text.
     * @return bool
     */
    private static function derive_needs_attention($status, $health, $is_ready, $action) {
        if (!$is_ready || in_array($health, array('degraded', 'invalid'), true) || '' !== $action) {
            return true;
        }

        return 'ready_to_publish' !== $status;
    }

    /** @return string */
    private static function default_health_for_status($status) {
        if (in_array($status, array('token_invalid', 'token_expired', 'configuration_error', 'environment_mismatch', 'publish_access_denied'), true)) {
            return 'invalid';
        }
        if ('ready_to_publish' === $status) {
            return 'healthy';
        }

        return 'unknown';
    }

    /**
     * Normalize a network slug without silently changing its meaning.
     *
     * @param mixed $value Network value.
     * @return string|WP_Error
     */
    private static function normalize_network($value) {
        if (!is_string($value)) {
            return new WP_Error('calliope_sp_connection_state_network', 'Connection network must be a string.');
        }

        $value = trim($value);
        if ('' === $value || false !== strpos($value, '://') || false !== strpos($value, '/') || false !== strpos($value, '\\')) {
            return new WP_Error('calliope_sp_connection_state_network', 'Connection network is invalid.');
        }
        if (!preg_match('/^[A-Za-z0-9_-]+$/', $value)) {
            return new WP_Error('calliope_sp_connection_state_network', 'Connection network contains unsupported characters.');
        }

        $normalized = sanitize_key($value);
        if ('' === $normalized) {
            return new WP_Error('calliope_sp_connection_state_network', 'Connection network is invalid.');
        }

        return $normalized;
    }

    /**
     * Normalize a value against an allowlist.
     *
     * @param mixed               $value   Candidate value.
     * @param array<string,mixed> $allowed Allowed values.
     * @param string              $field   Field name.
     * @return string|WP_Error
     */
    private static function normalize_allowlisted($value, array $allowed, $field) {
        if (!is_string($value)) {
            return new WP_Error('calliope_sp_connection_state_' . $field, 'Connection state field is invalid.');
        }

        $value = sanitize_key($value);
        if (!isset($allowed[$value])) {
            return new WP_Error('calliope_sp_connection_state_' . $field, 'Connection state field uses an unsupported value.');
        }

        return $value;
    }

    /**
     * Normalize an evidence level.
     *
     * @param mixed $value Evidence level.
     * @return string|WP_Error
     */
    private static function normalize_evidence($value) {
        if (!is_string($value)) {
            return new WP_Error('calliope_sp_connection_state_evidence', 'Connection evidence level is invalid.');
        }

        $value = strtoupper(trim($value));
        if (!isset(self::$evidence_ranks[$value])) {
            return new WP_Error('calliope_sp_connection_state_evidence', 'Connection evidence level is unsupported.');
        }

        return $value;
    }

    /**
     * Strictly normalize booleans.
     *
     * @param mixed  $value Candidate value.
     * @param string $field Field name.
     * @return bool|WP_Error
     */
    private static function normalize_boolean($value, $field) {
        if (true === $value || 1 === $value || '1' === $value) {
            return true;
        }
        if (false === $value || 0 === $value || '0' === $value) {
            return false;
        }

        return new WP_Error('calliope_sp_connection_state_' . $field, 'Connection state boolean is invalid.');
    }

    /**
     * Normalize integer timestamps.
     *
     * @param mixed  $value Candidate timestamp.
     * @param string $field Field name.
     * @return int|WP_Error
     */
    private static function normalize_timestamp($value, $field) {
        if (is_string($value) && preg_match('/^\d+$/', $value)) {
            $value = (int) $value;
        }
        if (!is_int($value) || $value < 0) {
            return new WP_Error('calliope_sp_connection_state_' . $field, 'Connection state timestamp is invalid.');
        }

        return $value;
    }

    /**
     * Normalize a short safe text field.
     *
     * @param mixed  $value  Candidate text.
     * @param int    $limit  Character limit.
     * @param string $field  Field name.
     * @return string|WP_Error
     */
    private static function normalize_text($value, $limit, $field) {
        if (!is_string($value)) {
            return new WP_Error('calliope_sp_connection_state_' . $field, 'Connection state text is invalid.');
        }

        $value = wp_strip_all_tags($value, true);
        $value = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $value);
        $value = preg_replace('/\s+/u', ' ', (string) $value);
        $value = trim((string) $value);
        if (function_exists('mb_substr')) {
            return mb_substr($value, 0, $limit, 'UTF-8');
        }

        return substr($value, 0, $limit);
    }

    /**
     * Build a consistent coherence error.
     *
     * @param string $message Safe internal message.
     * @return WP_Error
     */
    private static function coherence_error($message) {
        return new WP_Error('calliope_sp_connection_state_contradiction', $message);
    }
}
