package Blockchain::Ethereum::Keystore::Keyfile;

use v5.26;
use strict;
use warnings;

our $AUTHORITY = 'cpan:REFECO';    # AUTHORITY
our $VERSION   = '0.011';          # VERSION

use Carp;
use File::Slurp;
use JSON::MaybeXS qw(decode_json encode_json);
use Crypt::PRNG;
use Net::SSH::Perl::Cipher;

use Blockchain::Ethereum::Keystore::Key;
use Blockchain::Ethereum::Keystore::Keyfile::KDF;

sub new {
    my ($class, %params) = @_;

    my $self = bless {}, $class;
    for (qw(cipher ciphertext mac version iv kdf id private_key)) {
        $self->{$_} = $params{$_} if exists $params{$_};
    }

    return $self;
}

sub cipher {
    shift->{cipher};
}

sub ciphertext {
    shift->{ciphertext};
}

sub mac {
    shift->{mac};
}

sub version {
    shift->{version};
}

sub iv {
    shift->{iv};
}

sub kdf {
    shift->{kdf};
}

sub id {
    shift->{id};
}

sub private_key {
    shift->{private_key};
}

sub _json {
    return shift->{json} //= JSON::MaybeXS->new(utf8 => 1);
}

sub import_file {
    my ($self, $file_path, $password) = @_;

    my $content = read_file($file_path);
    my $decoded = $self->_json->decode(lc $content);

    return $self->_from_object($decoded, $password);
}

sub _from_object {
    my ($self, $object, $password) = @_;

    my $version = $object->{version};

    croak 'Version not supported' unless $version && $version == 3;

    return $self->_from_v3($object, $password);
}

sub _from_v3 {
    my ($self, $object, $password) = @_;

    my $crypto = $object->{crypto};

    $self->{cipher}     = 'AES128_CTR';
    $self->{ciphertext} = $crypto->{ciphertext};
    $self->{mac}        = $crypto->{mac};
    $self->{version}    = 3;
    $self->{iv}         = $crypto->{cipherparams}->{iv};

    my $header = $crypto->{kdfparams};

    $self->{kdf} = Blockchain::Ethereum::Keystore::Keyfile::KDF->new(
        algorithm => $crypto->{kdf},     #
        dklen     => $header->{dklen},
        n         => $header->{n},
        p         => $header->{p},
        r         => $header->{r},
        c         => $header->{c},
        prf       => $header->{prf},
        salt      => $header->{salt});

    $self->{private_key} = $self->_private_key($password);

    return $self;
}

sub change_password {
    my ($self, $old_password, $new_password) = @_;

    return $self->import_key($self->_private_key($old_password), $new_password);
}

sub _private_key {
    my ($self, $password) = @_;

    return $self->private_key if $self->private_key;

    my $cipher = Net::SSH::Perl::Cipher->new(
        $self->cipher,    #
        $self->kdf->decode($password),
        pack("H*", $self->iv));

    my $key = $cipher->decrypt(pack("H*", $self->ciphertext));

    return Blockchain::Ethereum::Keystore::Key->new(private_key => $key);
}

sub import_key {
    my ($self, $key, $password) = @_;

    # use the internal method here otherwise would not be availble to get the kdf params
    # salt if give will be the same as the response, if not will be auto generated by the library
    my ($derived_key, $salt, $N, $r, $p);
    ($derived_key, $salt, $N, $r, $p) = Crypt::ScryptKDF::_scrypt_extra($password);
    $self->kdf->{algorithm} = "scrypt";
    $self->kdf->{dklen}     = length $derived_key;
    $self->kdf->{n}         = $N;
    $self->kdf->{p}         = $p;
    $self->kdf->{r}         = $r;
    $self->kdf->{salt}      = unpack "H*", $salt;

    my $iv = Crypt::PRNG::random_bytes(16);
    $self->{iv} = unpack "H*", $iv;

    my $cipher = Net::SSH::Perl::Cipher->new(
        "AES128_CTR",    #
        $derived_key,
        $iv
    );

    my $encrypted = $cipher->encrypt($key->export);
    $self->{ciphertext} = unpack "H*", $encrypted;

    $self->{private_key} = $key;

    return $self;
}

sub _write_to_object {
    my $self = shift;

    croak "KDF algorithm and parameters are not set" unless $self->kdf;

    my $file = {
        "crypto" => {
            "cipher"       => 'aes-128-ctr',
            "cipherparams" => {"iv" => $self->iv},
            "ciphertext"   => $self->ciphertext,
            "kdf"          => $self->kdf->algorithm,
            "kdfparams"    => {
                "dklen" => $self->kdf->dklen,
                "n"     => $self->kdf->n,
                "p"     => $self->kdf->p,
                "r"     => $self->kdf->r,
                "salt"  => $self->kdf->salt
            },
            "mac" => $self->mac
        },
        "id"      => $self->id,
        "version" => 3
    };

    return $file;
}

sub write_to_file {
    my ($self, $file_path) = @_;

    return write_file($file_path, $self->_json->canonical(1)->pretty->encode($self->_write_to_object));
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Blockchain::Ethereum::Keystore::Keyfile

=head1 VERSION

version 0.011

=head1 SYNOPSIS

...

=head1 OVERVIEW

This is an Ethereum keyfile abstraction that provides a way of change/read the
keyfile information.

Currently only supports version 3 keyfiles.

=head1 METHODS

=head2 import_file

Import a keyfile (supports only version 3 as of now)

=over 4

=item * C<file_path> - string path for the keyfile

=back

self

=head2 change_password

Change the imported keyfile password

=over 4

=item * C<old_password> - Current password for the keyfile

=item * C<new_password> - New password to be set

=back

self

=head2 import_key

Import a L<Blockchain::Ethereum::keystore::Key>

=over 4

=item * C<keyfile> - L<Blockchain::Ethereum::Keystore::Key>

=back

self

=head2 write_to_file

Write the imported keyfile/private_key to a keyfile in the file system

=over 4

=item * C<file_path> - file path to save the data

=back

returns 1 upon successfully writing the file or undef if it encountered an error

=head1 AUTHOR

Reginaldo Costa <refeco@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2023 by REFECO.

This is free software, licensed under:

  The MIT (X11) License

=cut
