package Cinnamon::Context;
use strict;
use warnings;

use Moo;

use YAML ();
use Class::Load ();
use Hash::MultiValue;
use Term::ReadKey;

use Cinnamon;
use Cinnamon::Runner;
use Cinnamon::Logger;
use Cinnamon::Role;
use Cinnamon::Task;
use Cinnamon::Config::Loader;

our $CTX;

has roles => (
    is => 'ro',
    default => sub { Hash::MultiValue->new() }
);

has tasks => (
    is => 'ro',
    default => sub { Hash::MultiValue->new() }
);

has params => (
    is => 'ro',
    default => sub { Hash::MultiValue->new() }
);

sub run {
    my ($self, $role_name, $task_name, %opts)  = @_;

    Cinnamon::Config::Loader->load(config => $opts{config});

    if ($opts{info}) {
        $self->dump_info;
        return;
    }

    # set role name and task name
    CTX->set_param(role => $role_name);
    CTX->set_param(task => $task_name);

    # override setting
    for my $key (keys %{ $opts{override_settings} }) {
        CTX->set_param($key => $opts{override_settings}->{$key});
    }

    my $hosts  = $self->get_role_hosts($role_name);
    my $task   = $self->get_task($task_name);
    my $runner = $self->get_param('runner_class') || 'Cinnamon::Runner';

    unless (defined $hosts) {
        log 'error', "undefined role : '$role_name'";
        return;
    }
    unless (defined $task) {
        log 'error', "undefined task : '$task_name'";
        return;
    }

    Class::Load::load_class $runner;

    my $result = $runner->start($hosts, $task);
    my (@success, @error);

    for my $key (keys %{$result || {}}) {
        if ($result->{$key}->{error}) {
            push @error, $key;
        }
        else {
            push @success, $key;
        }
    }

    log success => sprintf(
        "\n========================\n[success]: %s",
        (join(', ', @success) || ''),
    );

    log error => sprintf(
        "[error]: %s",
        (join(', ', @error)   || ''),
    );

    return (\@success, \@error);
}

sub add_role {
    my ($self, $name, $hosts, $params) = @_;
    $params ||= {};
    my $role = Cinnamon::Role->new(
        name   => $name,
        hosts  => $hosts,
        params => Hash::MultiValue->new(%$params),
    );
    $self->roles->set($name => $role);
}

sub get_role {
    my ($self, $name) = @_;
    return $self->roles->get($name);
}

sub get_role_hosts {
    my ($self, $name) = @_;
    my $role  = $self->get_role($name) or return undef;
    my $hosts = $role->get_hosts;

    # set role params
    # TODO: move from here
    my $params = $role->params;
    for my $key (keys %$params) {
        $self->set_param($key => $params->{$key});
    }

    return $hosts;
}

sub add_task {
    my ($self, $name, $code) = @_;
    unless (ref $code eq 'HASH') {
        my $task = Cinnamon::Task->new(
            name => $name,
            code => $code,
        );
        return $self->tasks->set($name => $task);
    }

    # a nest task is named as joined by colon
    for my $child (keys %$code) {
        my $child_name = join ":", $name, $child;
        $self->add_task($child_name => $code->{$child});
    }
}

sub get_task {
    my ($self, $name) = @_;
    return $self->tasks->get($name);
}

sub set_param {
    my ($self, $key, $value) = @_;
    $self->params->set($key => $value);
}

sub get_param {
    my ($self, $key, @args) = @_;

    my $value = $self->params->get($key);
    $value = $value->(@args) if ref $value eq 'CODE';

    return $value;
}

# Thread-specific stash
sub stash {
    my $stash = $Coro::current->{Cinnamon} ||= {};
}

sub call_task {
    my ($self, $task_name, $host) = @_;
    my $task = $self->get_task($task_name) or die "undefined task : '$task_name'";

    $task->execute($host);
}

sub run_cmd {
    my ($self, $commands, $opts) = @_;
    $opts ||= {};

    my $current_host = $self->stash->{current_host} || 'localhost';
    log info => sprintf "[%s :: executing] %s", $current_host, join(' ', @$commands);

    if ($opts->{sudo}) {
        $opts->{password} = $self->_get_sudo_password();
    }

    $opts->{tty} = !! $self->get_param('tty');

    my $executor = $self->build_command_executor;
    my $result = $executor->execute($commands, $opts);

    if ($result->{has_error}) {
        die sprintf "error status: %d", $result->{error};
    }

    return ($result->{stdout}, $result->{stderr});
}

sub build_command_executor {
    my ($self) = @_;

    if (my $remote = $self->stash->{current_remote}) {
        return $remote;
    }
    else {
        return Cinnamon::Local->new;
    }
}

sub dump_info {
    my ($self) = @_;
    my $info = {};

    my $roles = $self->roles;
    my $role_info = +{
        map { $_->name => $_->info } $roles->values,
    };

    my $tasks = $self->tasks;
    my $task_info = +{
        map { %{ $_->info } } $tasks->values,
    };

    log 'info', YAML::Dump({
        roles => $role_info,
        tasks => $task_info,
    });
}

sub _get_sudo_password {
    my ($self) = @_;
    my $password = $self->get_param('password');
    return $password if defined $password;

    print "Enter sudo password: ";
    ReadMode "noecho";
    chomp($password = ReadLine 0);
    ReadMode 0;
    print "\n";

    $self->set_param(password => $password);
    return $password;
}

!!1;