package App::Netdisco::DB::ResultSet::DevicePort;
use base 'App::Netdisco::DB::ResultSet';

use strict;
use warnings;

use Try::Tiny;
require Dancer::Logger;

__PACKAGE__->load_components(qw/
  +App::Netdisco::DB::ExplicitLocking
/);

=head1 ADDITIONAL METHODS

=head2 with_times

This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:

=over 4

=item lastchange_stamp

=back

=cut

sub with_times {
  my ($rs, $cond, $attrs) = @_;

  return $rs
    ->search_rs($cond, $attrs)
    ->search({},
      {
        '+columns' => { lastchange_stamp =>
          \("to_char(device.last_discover - (device.uptime - me.lastchange) / 100 * interval '1 second', "
            ."'YYYY-MM-DD HH24:MI:SS')") },
        join => 'device',
      });
}

=head2 with_is_free

This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:

=over 4

=item is_free

=back

In the C<$cond> hash (the first parameter) pass in the C<age_num> which must
be an integer, and the C<age_unit> which must be a string of either C<days>,
C<weeks>, C<months> or C<years>.

=cut

sub with_is_free {
  my ($rs, $cond, $attrs) = @_;

  my $interval = (delete $cond->{age_num}) .' '. (delete $cond->{age_unit});

  return $rs
    ->search_rs($cond, $attrs)
    ->search({},
      {
        '+columns' => { is_free =>
          # NOTE this query is in `git grep 'THREE PLACES'`
          \["me.up_admin = 'up' AND me.up != 'up' AND "
              ."(me.type IS NULL OR me.type !~* '^(53|ieee8023adLag|propVirtual|l2vlan|l3ipvlan|135|136|137)\$') AND "
              ."((age(LOCALTIMESTAMP, to_timestamp(extract(epoch from device.last_discover) - (device.uptime/100))::timestamp) < ?::interval "
              ."AND (last_node.time_last IS NULL OR age(LOCALTIMESTAMP, last_node.time_last) > ?::interval)) "
              ."OR age(LOCALTIMESTAMP, to_timestamp(extract(epoch from device.last_discover) - (device.uptime - me.lastchange)/100)::timestamp) > ?::interval)",
            [{} => $interval],[ {} => $interval],[ {} => $interval]] },
        join => [qw/device last_node/],
      });
}

=head2 only_free_ports

This is a modifier for any C<search()> (including the helpers below) which
will restrict results based on whether the port is considered "free".

In the C<$cond> hash (the first parameter) pass in the C<age_num> which must
be an integer, and the C<age_unit> which must be a string of either C<days>,
C<weeks>, C<months> or C<years>.

=cut

sub only_free_ports {
  my ($rs, $cond, $attrs) = @_;

  my $interval = (delete $cond->{age_num}) .' '. (delete $cond->{age_unit});

  return $rs
    ->search_rs($cond, $attrs)
    ->search(
      {
        # NOTE this query is in `git grep 'THREE PLACES'`
        'me.up_admin' => 'up',
        'me.up'       => { '!=' => 'up' },
        'me.type' => [ '-or' =>
          { '=' => undef },
          { '!~*' => '^(53|ieee8023adLag|propVirtual|l2vlan|l3ipvlan|135|136|137)$' },
        ],
        -or => [
          -and => [
            \["age(LOCALTIMESTAMP, to_timestamp(extract(epoch from device.last_discover) - (device.uptime/100))::timestamp) < ?::interval",
              [{} => $interval]],
            -or => [
              'last_node.time_last' => undef,
              \["age(LOCALTIMESTAMP, last_node.time_last) > ?::interval", [{} => $interval]],
            ]
          ],
          \["age(LOCALTIMESTAMP, to_timestamp(extract(epoch from device.last_discover) - (device.uptime - me.lastchange)/100)::timestamp) > ?::interval",
            [{} => $interval]],
        ],
      },{ join => [qw/device last_node/] },
    );
}

=head2 with_properties

This is a modifier for any C<search()> which
will add the following additional synthesized columns to the result set:

=over 4

=item error_disable_cause

=item remote_is_discoverable (boolean)

=item remote_is_wap (boolean)

=item remote_is_phone (boolean)

=item remote_dns

=item ifindex

=back

=cut

sub with_properties {
  my ($rs, $cond, $attrs) = @_;

  return $rs
    ->search_rs($cond, $attrs)
    ->search({},
      {
        '+select' => [qw/
          properties.error_disable_cause
          properties.remote_is_discoverable
          properties.remote_is_wap
          properties.remote_is_phone
          properties.remote_dns
          properties.ifindex
          properties.pae_authconfig_port_control
          properties.pae_authconfig_state
          properties.pae_authconfig_port_status
          properties.pae_authsess_user
          properties.pae_authsess_mab
          properties.pae_last_eapol_frame_source
        /],
        '+as' => [qw/
          error_disable_cause
          remote_is_discoverable remote_is_wap remote_is_phone remote_dns
          ifindex 
          pae_authconfig_port_control 
          pae_authconfig_state 
          pae_authconfig_port_status
          pae_authsess_user 
          pae_authsess_mab
          pae_last_eapol_frame_source
        /],
        join => 'properties',
      });
}

=head2 with_remote_inventory

This is a modifier for any C<search()> which
will add the following additional synthesized columns to the result set:

=over 4

=item remote_vendor

=item remote_model

=item remote_os_ver

=item remote_serial

=back

=cut

sub with_remote_inventory {
  my ($rs, $cond, $attrs) = @_;

  return $rs
    ->search_rs($cond, $attrs)
    ->search({},
      {
        '+select' => [qw/
          properties.remote_vendor
          properties.remote_model
          properties.remote_os_ver
          properties.remote_serial
        /],
        '+as' => [qw/
          remote_vendor remote_model remote_os_ver remote_serial
        /],
        join => 'properties',
      });
}

=head2 with_vlan_count

This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:

=over 4

=item vlan_count

=back

=cut

sub with_vlan_count {
  my ($rs, $cond, $attrs) = @_;

  return $rs
    ->search_rs($cond, $attrs)
    ->search({},
      {
        '+columns' => { vlan_count =>
          $rs->result_source->schema->resultset('DevicePortVlan')
            ->search(
              {
                'dpv.ip'   => { -ident => 'me.ip' },
                'dpv.port' => { -ident => 'me.port' },
              },
              { alias => 'dpv' }
            )->count_rs->as_query
        },
      });
}

=head1 SPECIAL METHODS

=head2 delete( \%options? )

Overrides the built-in L<DBIx::Class> delete method to more efficiently
handle the removal or archiving of nodes.

=cut

sub _plural { (shift || 0) == 1 ? 'entry' : 'entries' };

sub delete {
  my $self = shift;

  my $schema = $self->result_source->schema;
  my $ports = $self->search(undef, { columns => 'ip' });

  my $ip = undef;
  {
    no autovivification;
    try { $ip ||= ${ $ports->{attrs}->{where}->{ip}->{'-in'} }->[1]->[1] };
    try { $ip ||= $ports->{attrs}->{where}->{'me.ip'} };
  }
  $ip ||= 'netdisco';

  foreach my $set (qw/
    DevicePortPower
    DevicePortProperties
    DevicePortSsid
    DevicePortVlan
    DevicePortWireless
  /) {
      my $gone = $schema->resultset($set)->search(
        { ip => { '-in' => $ports->as_query }},
      )->delete;

      Dancer::Logger::debug( sprintf( ' [%s] db/ports - removed %d port %s from %s',
        $ip, $gone, _plural($gone), $set ) ) if defined Dancer::Logger::logger();
  }

  $schema->resultset('Node')->search(
    { switch => { '-in' => $ports->as_query }},
  )->delete(@_);

  # now let DBIC do its thing
  return $self->next::method();
}

1;
