<?php

namespace UltraguardPro\modules\authenticator\db;

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

use UltraguardPro\db\BaseTable;
use UltraguardPro\core\DatabaseCrud;

class TotpCredentialsTable extends BaseTable {

    protected $table_name  = 'ultraguard_totp_credentials';
    protected $cache_group = 'ultraguard_totp_credentials';

    public function get_defaults(): array {
        return [
            'id'              => 0,
            'user_id'         => 0,
            'secret_key'      => '',
            'backup_codes'    => null,
            'is_enabled'      => 0,
            'is_enforced'     => 0,
            'last_used_at'    => null,
            'last_used_code'  => '',
            'failed_attempts' => 0,
            'locked_until'    => null,
            'created_at'      => '',
            'updated_at'      => '',
        ];
    }

    protected function validate_data( $data ) {
        if ( isset( $data['user_id'] ) && empty( $data['user_id'] ) ) {
            return false;
        }

        return $data;
    }

    protected function sanitize_data( $data ): array {
        $out = [];

        foreach ( $data as $key => $value ) {
            if ( in_array( $key, [ 'user_id', 'failed_attempts' ], true ) ) {
                $out[ $key ] = absint( $value );
            } elseif ( in_array( $key, [ 'is_enabled', 'is_enforced' ], true ) ) {
                $out[ $key ] = (int) (bool) $value;
            } elseif ( 'backup_codes' === $key ) {
                $out[ $key ] = is_null( $value ) ? null : ( is_array( $value ) ? wp_json_encode( $value ) : sanitize_textarea_field( (string) $value ) );
            } elseif ( in_array( $key, [ 'locked_until', 'last_used_at' ], true ) ) {
                $out[ $key ] = empty( $value ) ? null : sanitize_text_field( (string) $value );
            } elseif ( in_array( $key, [ 'last_used_code', 'secret_key' ], true ) ) {
                $out[ $key ] = sanitize_text_field( (string) $value );
            } else {
                $out[ $key ] = is_string( $value ) ? sanitize_text_field( $value ) : $value;
            }
        }

        return $out;
    }

    public function get_for_user( int $user_id ): ?array {
        $row = $this->get_row( [ 'user_id' => $user_id ] );
        return ! empty( $row['id'] ) ? $row : null;
    }

    public function upsert_user( int $user_id, array $data ): bool {
        $existing = $this->get_for_user( $user_id );
        $payload  = array_merge( [ 'user_id' => $user_id ], $data );

        if ( $existing ) {
            return (bool) $this->update( (int) $existing['id'], $payload );
        }

        return (bool) $this->insert( $payload );
    }

    public function disable_for_user( int $user_id ): bool {
        $row = $this->get_for_user( $user_id );
        if ( ! $row ) {
            return false;
        }

        return (bool) $this->update( (int) $row['id'], [
            'is_enabled'      => 0,
            'failed_attempts' => 0,
            'locked_until'    => null,
        ] );
    }

    public function replace_backup_codes( int $user_id, array $hashes ): bool {
        return $this->upsert_user( $user_id, [ 'backup_codes' => wp_json_encode( array_values( $hashes ) ) ] );
    }

    public function consume_backup_code( int $user_id, string $code ): bool {
        $row = $this->get_for_user( $user_id );
        if ( ! $row ) {
            return false;
        }

        $codes = json_decode( (string) $row['backup_codes'], true );
        if ( ! is_array( $codes ) ) {
            return false;
        }

        $code = strtoupper( sanitize_text_field( $code ) );

        foreach ( $codes as $index => $stored_hash ) {
            if ( ! is_string( $stored_hash ) || '' === $stored_hash ) {
                continue;
            }

            if ( password_verify( $code, $stored_hash ) ) {
                unset( $codes[ $index ] );

                return (bool) $this->update( (int) $row['id'], [
                    'backup_codes'    => wp_json_encode( array_values( $codes ) ),
                    'failed_attempts' => 0,
                    'locked_until'    => null,
                ] );
            }
        }

        return false;
    }

    public function mark_verified( int $user_id, string $code ): bool {
        $row = $this->get_for_user( $user_id );
        if ( ! $row ) {
            return false;
        }

        return (bool) $this->update( (int) $row['id'], [
            'last_used_at'    => gmdate( 'Y-m-d H:i:s' ),
            'last_used_code'  => substr( sanitize_text_field( $code ), 0, 10 ),
            'failed_attempts' => 0,
            'locked_until'    => null,
            'is_enabled'      => 1,
        ] );
    }

    public function record_failed_attempt( int $user_id ): int {
        $row      = $this->get_for_user( $user_id );
        $attempts = (int) ( $row['failed_attempts'] ?? 0 ) + 1;
        $locked   = $attempts >= 5 ? gmdate( 'Y-m-d H:i:s', time() + 15 * MINUTE_IN_SECONDS ) : null;

        if ( $row ) {
            $this->update( (int) $row['id'], [
                'failed_attempts' => $attempts,
                'locked_until'    => $locked,
            ] );
        }

        return $attempts;
    }

    public function is_locked( int $user_id ): bool {
        $row = $this->get_for_user( $user_id );
        if ( empty( $row['locked_until'] ) ) {
            return false;
        }

        return strtotime( $row['locked_until'] . ' UTC' ) > time();
    }

    public function clear_expired_locks(): void {
        $rows = DatabaseCrud::get_records_before_datetime(
            'ultraguard_totp_credentials',
            'locked_until',
            gmdate( 'Y-m-d H:i:s' ),
            [],
            500,
            'locked_until',
            'ASC',
            ARRAY_A,
            true
        );

        foreach ( $rows as $row ) {
            if ( empty( $row['id'] ) ) {
                continue;
            }

            $this->update( (int) $row['id'], [
                'failed_attempts' => 0,
                'locked_until'    => null,
            ] );
        }
    }

    public function set_enabled_flag( int $user_id, int $enabled ): bool {
        $row = $this->get_for_user( $user_id );
        if ( ! $row ) {
            return false;
        }

        return (bool) $this->update( (int) $row['id'], [ 'is_enabled' => $enabled ] );
    }

    public function is_enabled_for_user( int $user_id ): bool {
        $row = $this->get_for_user( $user_id );
        return $row ? ! empty( $row['is_enabled'] ) : false;
    }

    public function is_role_enforced( int $user_id, array $required_roles ): bool {
        $user = get_userdata( $user_id );
        return $user && ! empty( array_intersect( (array) $user->roles, $required_roles ) );
    }

    public function count_enabled_users(): int {
        return (int) $this->count( [ 'is_enabled' => 1 ] );
    }

    public function count_enforced_users(): int {
        return (int) $this->count( [ 'is_enforced' => 1 ] );
    }
}
