package AsyncLogWatcher;

use strict;
use warnings;

our $VERSION = '0.05';

use File::Tail;

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

    my $self = {
        log_file_path    => $args->{log_file_path} || die "log_file_path is mandatory",
        patterns_file    => $args->{patterns_file} || die "patterns_file is mandatory",
        exclude_file     => $args->{exclude_file} || die "exclude_file is mandatory",
        on_match         => $args->{on_match} // sub { print "Matched line: $_[0]\n" },
        patterns_re      => undef,
        exclude_patterns => [],
    };

    bless $self, $class;

    $self->load_patterns();
    $self->load_exclude_patterns();

    $self->{file_tail} = File::Tail->new(name => $self->{log_file_path}, maxinterval => 3, adjustafter => 7);

    return $self;
}

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

    open(my $pfh, '<', $self->{patterns_file}) or die "Could not open file '$self->{patterns_file}' $!";
    my @patterns = <$pfh>;
    chomp @patterns;
    close($pfh);

    $self->{patterns_re} = join '|', map quotemeta, @patterns;
    $self->{patterns_re} = qr/$self->{patterns_re}/i;
}

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

    open(my $efh, '<', $self->{exclude_file}) or die "Could not open file '$self->{exclude_file}' $!";
    my @exclude_patterns = <$efh>;
    chomp @exclude_patterns;
    close($efh);

    $self->{exclude_patterns} = [ map { qr/$_/i } @exclude_patterns ];
}

sub is_excluded {
    my ($self, $line) = @_;

    for my $exclude_re (@{ $self->{exclude_patterns} }) {
        return 1 if $line =~ $exclude_re;
    }

    return 0;
}

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

    while (defined(my $line = $self->{file_tail}->read)) {
        if ($line =~ $self->{patterns_re} && !$self->is_excluded($line)) {
            $self->{on_match}->($line);
        }
    }
}

1;

=head1 NAME

AsyncLogWatcher - Perl module for async log watching

=head1 SYNOPSIS

  use AsyncLogWatcher;
  
  my $watcher = AsyncLogWatcher->new({
    log_file_path    => "/path/to/logfile",
    patterns_file    => "/path/to/patterns",
    exclude_file     => "/path/to/exclude",
    on_match         => sub { print "Matched line: $_[0]\n" },
  });

  $watcher->watch();

=head1 DESCRIPTION

The AsyncLogWatcher module provides functionality for asynchronously watching a log file and applying matching and exclusion patterns to its lines. It can be used to scan logs and perform an action when a matching line is found, ignoring lines that match any of the exclusion patterns.

=head1 METHODS

=head2 new

  my $watcher = AsyncLogWatcher->new({
    log_file_path    => "/path/to/logfile",
    patterns_file    => "/path/to/patterns",
    exclude_file     => "/path/to/exclude",
    on_match         => sub { print "Matched line: $_[0]\n" },
  });

Constructs a new AsyncLogWatcher object. Requires the paths to the log file, patterns file, and exclusion file, as well as an optional callback that will be executed when a matching line is found.

=head2 load_patterns

  $watcher->load_patterns();

Loads the matching patterns from the patterns file. Patterns are loaded as a regular expression.

=head2 load_exclude_patterns

  $watcher->load_exclude_patterns();

Loads the exclusion patterns from the exclusion file. Patterns are loaded as a regular expression.

=head2 is_excluded

  my $excluded = $watcher->is_excluded($line);

Checks whether a line matches any of the loaded exclusion patterns. Returns a boolean value.

=head2 watch

  $watcher->watch();

Starts watching the log file. The method loops indefinitely, checking for new lines in the log file. When a line matches the loaded patterns and does not match any of the exclusion patterns, the provided callback is executed with the line as an argument.

=head1 AUTHOR

Kawamura Shingo <pannakoota@gmail.com>

=head1 VERSION

Version 0.04

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2023 Your Name

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut

