package AsyncLogWatcher;

=head1 NAME

AsyncLogWatcher - Asynchronously watch log files for specific patterns.

=head1 SYNOPSIS

  use AsyncLogWatcher;

  my $watcher = AsyncLogWatcher->new({
      log_dir       => "/var/log",
      log_file_name => "messages",
      patterns_file => "patterns.txt",
      exclude_file  => "exclude.txt",
  });

  $watcher->watch();

=head1 DESCRIPTION

AsyncLogWatcher is a module that watches a log file for lines matching a given set of patterns. 
When a line matches one of the patterns and does not match any of the exclude patterns, 
a callback function is called with the matched line as an argument.

=cut

use strict;
use warnings;

our $VERSION = '0.02';

use AnyEvent;
use Linux::Inotify2;

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

    my $self = {
        log_dir          => $args->{log_dir} || '/var/log',
        log_file_name    => $args->{log_file_name} || 'messages',
        patterns_file    => $args->{patterns_file} || 'patterns.txt',
        exclude_file     => $args->{exclude_file} || 'exclude.txt',
        on_match         => $args->{on_match} // sub { print "Matched line: $_[0]" },
        patterns_re      => undef,
        exclude_patterns => [],
        cv               => AnyEvent->condvar,
        inotify          => AnyEvent::Inotify2->new,
    };

    bless $self, $class;

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

    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 watch_file {
    my ($self, $file_path) = @_;

    open my $fh, '<', $file_path
        or die "Unable to open log file: $!";

    seek $fh, 0, 2;

    $self->{inotify}->watch(
        $file_path,
        IN_MODIFY,
        sub {
            my ($e) = @_;
            if ($e->name eq $self->{log_file_name} and $e->IN_MODIFY) {
                while (my $line = <$fh>) {
                    if ($line =~ $self->{patterns_re} && !$self->is_excluded($line)) {
                        $self->{on_match}->($line);
                    }
                }
            }
        }
    );
}

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) = @_;

    $self->watch_file("$self->{log_dir}/$self->{log_file_name}");

    $self->{inotify}->watch(
        $self->{log_dir},
        IN_MOVED_TO,
        sub {
            my ($e) = @_;
            if ($e->name eq $self->{log_file_name}) {
                $self->watch_file("$self->{log_dir}/$self->{log_file_name}");
            }
        }
    );

    $self->{inotify}->watch(
        $self->{patterns_file},
        IN_MODIFY,
        sub {
            $self->load_patterns();
        }
    );

    $self->{inotify}->watch(
        $self->{exclude_file},
        IN_MODIFY,
        sub {
            $self->load_exclude_patterns();
        }
    );

    $self->{cv}->recv;
}

=head1 METHODS

=over 4

=item new( \%args )

Create a new AsyncLogWatcher instance.

  my $watcher = AsyncLogWatcher->new({
      log_dir       => "/var/log",         # Directory of the log file
      log_file_name => "messages",         # Name of the log file
      patterns_file => "patterns.txt",     # File containing patterns to match
      exclude_file  => "exclude.txt",      # File containing patterns to exclude
  });

=back

=head1 CALLBACKS

=over 4

=item on_match

A callback that is called when a line matches one of the patterns and does not match any of the exclude patterns.
The matched line is passed as an argument.

  sub {
      my ($matched_line) = @_;
      print "Matched line: $matched_line";
  }

=back

=head1 AUTHOR

Your Name <your-email@example.com>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2023 by Your Name

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

=cut

1;

