package Exception::API::ADVQ;
{
  $Exception::API::ADVQ::VERSION = '0.001';
}
use base qw(Exception);

package Exception::API::ADVQ::HTTP;
{
  $Exception::API::ADVQ::HTTP::VERSION = '0.001';
}
use base qw(Exception::API::ADVQ);

package Exception::API::ADVQ::TAINTED;
{
  $Exception::API::ADVQ::TAINTED::VERSION = '0.001';
}
use base qw(Exception::API::ADVQ);

package Exception::API::ADVQ::HostConfig;
{
  $Exception::API::ADVQ::HostConfig::VERSION = '0.001';
}
use base qw(Exception::API::ADVQ);

=head1 Name

QBit::Application::Model::API::ADVQ - AdvQ's API realization.

=head1 Config opions

=over

=item

B<URLs> - hash, server list. Like

 URLs => {
     server_name => {
         search => [map {"http://test$_.advq.yandex.ru"} 1..6],
         chrono => [map {"http://test$_.advq.yandex.ru"} 1..6],
     }
 }

Server name gets by `hostname` (without parameters).

=back

=head1 Required models

=over

=item

B<memcached> => L<QBit::Application::Model::Memcached>.

=back

=cut

package QBit::Application::Model::API::Yandex::ADVQ;
{
  $QBit::Application::Model::API::Yandex::ADVQ::VERSION = '0.001';
}

use qbit;

use base qw(QBit::Application::Model);

use LWP::UserAgent;
use YAML::XS;
use Net::INET6Glue;

__PACKAGE__->model_accessors(memcached => 'QBit::Application::Model::Memcached');

our %TR_PARAM_LANG = (
    'ru'    => 'RUS',
    'tr'    => 'TUR',
);

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

    $self->SUPER::init();

    $self->{__LWP__} = LWP::UserAgent->new(timeout => 60);
    $self->{__LWP__}->default_header('X-Advq-Customer' => 'wordstat');
    $self->{__DEBUG__} = $self->get_option('debug', 0);

    $self->{hostname} = `hostname`;
    chomp($self->{hostname});
}

=head1 Methods

=head2 search

Get data from basic DB.

B<Arguments as hash>

=over

=item

B<words> - string or ref to array of string, requested phrases, required;

=item

B<regions> - string, region list ids, joined by ',';

=item

B<ph_page> - number, results page number, from zero;

=item

B<ph_page_size> - number, results page size;

=item

B<count_by_regions> - boolean, include into result additional statistic grouped by regions;

=item

B<dblist> - ???;

=item

B<parser> - string, parser name. One of C<advq>, C<direct>.

=back

B<Return value:> ref to array of hashes. Structure depends on parameters.

=cut

sub search {
    my ($self, %opts) = @_;

    $opts{'lang'} = $self->_get_param_lang(%opts);
    $opts{'tld'}  = $self->_get_param_tld(%opts);

    return $self->_search(%opts);
}

sub search7 {
    my ($self, %opts) = @_;

    $opts{'dbname'} = $self->_get_param_dbname(%opts);
    $opts{'devices'} = $self->_get_param_devices(%opts);

    return $self->_search(%opts);
}

sub _search {
    my ($self, %opts) = @_;

    my @param_pairs = (
        $self->_make_words_param_pairs($opts{'words'}),
        (
            map  {"$_=" . uri_escape($opts{$_})}
            grep {defined($opts{$_})} qw(regions ph_page ph_page_size count_by_regions dblist lang tld dbname devices parser)
        )
    );

    if (($opts{ph_page} || 0) > 40) {
        return [];
    }

    my $data = $self->_get_data(search => '/advq/search?' . join('&', @param_pairs));

    if (($opts{ph_page} || 0) > ($data->[0]->{stat}->{page_count} || 1)) {
        return [];
    }

    return $data;
}

=head2 chrono

Get data from chrono DB.

B<Arguments as hash:>

=over

=item

B<words> - string or ref to array of string, requested phrases, required;

=item

B<type> - string, period name. One of C<monthly>, C<weekly>, C<daily>;

=item

B<regions> - string, region list ids, joined by ',';

=item

B<make_reg_gist> - ???;

=item

B<parser> - string, parser name. One of C<advq>, C<direct>.

=back

B<Return value:> ref to array of hashes. Structure depends on parameters.

=cut

sub chrono {
    my ($self, %opts) = @_;

    $opts{'lang'} = $self->_get_param_lang(%opts);
    $opts{'tld'}  = $self->_get_param_tld(%opts);

    return $self->_chrono(%opts);
}

sub chrono7 {
    my ($self, %opts) = @_;

    $opts{'dbname'} = $self->_get_param_dbname(%opts);
    $opts{'devices'} = $self->_get_param_devices(%opts);

    return $self->_chrono(%opts);
}

sub _chrono {
    my ($self, %opts) = @_;

    $opts{'type'} //= 'monthly';

    my @param_pairs = (
        $self->_make_words_param_pairs($opts{'words'}),
        (map {"$_=" . uri_escape($opts{$_})} grep {defined($opts{$_})} qw(regions make_reg_gist lang tld dbname devices parser))
    );

    return $self->_get_data(chrono => "/advq/$opts{'type'}_hist?" . join('&', @param_pairs));
}

sub _make_words_param_pairs {
    my ($self, $words) = @_;

    throw Exception::BadArguments gettext('Missed required argument "word"') unless defined($words);
    $words = [$words] unless ref($words);

    foreach (@$words) {
        $_ = lc($_);
        s/ +- / /g;
    }

    return map {'words=' . uri_escape($_)} @$words;
}

sub _get_param_lang {
    my ($self, %opts) = @_;
    our %TR_PARAM_LANG;

    my $locale = $opts{'locale'} || $self->get_option('locale') || 'ru';
    my $lang = $opts{'lang'} || $TR_PARAM_LANG{$locale} || $TR_PARAM_LANG{'ru'};

    throw Exception::BadArguments gettext('Wrong parameter "lang"') unless $lang && in_array($lang, [values(%TR_PARAM_LANG)]);

    return $lang;
}

sub _get_param_tld {
    my ($self, %opts) = @_;

    my $tld = $opts{'tld'};
    $tld = $1 if !$tld && $self->get_option('yandex_tld', '') =~ /([^\.]+)$/;
    $tld = 'ru' unless $tld;

    throw Exception::BadArguments gettext('Wrong parameter "tld"') unless $tld && $tld =~ /^[a-z]+$/;

    return $tld;
}

sub _get_param_dbname {
    my ($self, %opts) = @_;

    throw Exception::BadArguments gettext('Bad "dbname" ('.$opts{'dbname'}.')') unless in_array($opts{'dbname'} || 'rus', ['rus', 'tur', 'rus-mobile', 'tur-mobile']);
    delete $opts{'lang'};
    delete $opts{'tld'};

    return $opts{'dbname'};
}

sub _get_param_devices {
    my ($self, %opts) = @_;

    # TODO: support "devices=phone,tablet"
    throw Exception::BadArguments gettext('Bad "devices" ('.$opts{'devices'}.')') unless in_array($opts{'devices'} || 'all', ['phone', 'tablet', 'desktop', 'all', 'phone,tablet']);

    return $opts{'devices'};
}

sub _get_url {
    my ($self, $type) = @_;

    my $urls = $self->get_option('URLs')->{'*'}{$type}
      // throw Exception::API::ADVQ::HostConfig gettext(
        'Type "%s" for hostname "%s" has not found in configuration file', $type, $self->{'hostname'});

    $self->{'current_url_index'}{$type} //= int(rand(@$urls));
    $self->{'current_url_index'}{$type} = 0 if ++$self->{'current_url_index'}{$type} >= @$urls;

    my $url = $urls->[$self->{'current_url_index'}{$type}];

    return $url;
}

sub _get_data {
    my ($self, $url_type, $path_params) = @_;

    # try get from cache
    # my ($cached_data, $cache_version) = @{$self->memcached->get($url_type, [$path_params, 'cache_version'])};
    my $cached_data = $self->memcached->get($url_type, $path_params);
    my $cache_version = $self->memcached->get($url_type, 'cache_version');
    $self->_l("TRY 1: FROM CACHE. hash:('$url_type, $path_params') data:" . ($cached_data ? 'present' : '-') . " version:" . ($cache_version || '-'));
    if (
        $cached_data && ref($cached_data) eq 'ARRAY' && $cached_data->[0]->{stat}
        &&
        $cache_version && $cache_version eq $cached_data->[0]->{stat}->{db_date_str}
    ) {
        # is cache fresh?
        $self->_l("Cache - ok");
        return $cached_data;
    }

    # try get from backend
    my ($response, $data, $db_version);
    my ($url_path, $url_params) = (split(/\?/, $path_params, 2), '', '');
    my $url_host = $self->_get_url($url_type);

    for (1 .. 3) {
        $self->_l("TRY $_: POST '$url_host' '$url_path' '$url_params'");
        $response = $self->{__LWP__}->post($url_host . $url_path, Content => $url_params);

        if ($response->is_success()) {
            $data = $response->decoded_content('default_charset' => 'UTF-8');
            utf8::encode($data);    # turn into octets
            $data = Load($data)->{'requests'};
            $data = __fix_utf($data);
            $self->_l("TRY $_: ok. data length - " . scalar(@$data));
            last unless grep {$_->{'stat'}{'tainted'} || $_->{'tainted'}} @$data;
        } else {
            $self->_l("TRY $_: fail. Response: " . $response->status_line() . " - " . $response->message());
            l("$_: $url_host$url_path $url_params. Response status: " . $response->status_line());
            last if $response->message() =~ /timeout/i;
        }
    }

    throw Exception::API::ADVQ::HTTP gettext('Cannot get data from ADVQ: "%s"', $response->status_line)
      unless defined($data);

    # extra data
    $_->{model}->{backend_url} = $url_host . $url_path . ' ' . $url_params foreach @$data;
    ($db_version) = map {$_->{'stat'}{'db_date_str'} || ()} @$data;
    $db_version //= '';

    throw Exception::API::ADVQ::TAINTED gettext('Server has returned tainted answer')
      if grep {$_->{'stat'}{'tainted'} || $_->{'tainted'}} @$data;

    # create cache
    $self->memcached->set($url_type, $path_params, $data, 60 * 60 * 24);
    if (!$cache_version || $cache_version ne $db_version) {
        $self->memcached->set($url_type, 'cache_version', $db_version, 60 * 60 * 24);
    }

    return $data;
}

sub _l {
    my ($self, @msg) = @_;
    l(join("\t", @msg)) if $self->{'__DEBUG__'};
}

sub __fix_utf {
    my ($data) = @_;

    if (ref($data) eq '') {
        utf8::decode($data);
        return $data;
    } elsif (ref($data) eq 'ARRAY') {
        return [map {__fix_utf($_)} @$data];
    } elsif (ref($data) eq 'HASH') {
        return {map {$_ => __fix_utf($data->{$_})} keys(%$data)};
    } else {
        throw 'Unknown ref: ' . ref($data);
    }
}

TRUE;
