package ConfigServer::Terminal;
use strict;
use warnings;
use CGI qw(:standard -utf8);
use Fcntl qw(SEEK_SET :flock);
use File::stat;
use IPC::Open3;
use Symbol 'gensym';
use JSON::PP qw(encode_json);

our @EXPORT_OK = qw(terminal);

# ===== Config/const =====
# Când rulează încorporat în csf.cgi/configserver.cgi NU trimitem headere proprii
my $EMBEDDED      = 1;
my $DEFAULT_LINES = 100;          # număr linii implicit
my $PING_OK       = { ok => JSON::PP::true };

# ===== Utils =====
sub _e {                           # escape pt text în <pre>
    my $s = defined $_[0] ? $_[0] : '';
    $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g;
    return $s;
}

sub _header_json {
    return if $EMBEDDED;
    my $q = CGI->new; print $q->header(-type => 'application/json', -charset => 'utf-8');
}

sub _header_html {
    return if $EMBEDDED;
    my $q = CGI->new; print $q->header(-type => 'text/html', -charset => 'utf-8');
}

sub _slurp {
    my ($file) = @_;
    return () unless defined $file && -r $file;
    open(my $FH, '<', $file) or return ();
    flock($FH, LOCK_SH);
    my @d = <$FH>;
    close($FH);
    return @d;
}

sub _script_url {
    # Folosește SCRIPT_NAME care include deja /cpsessXXXX când e cazul
    my $sn = $ENV{SCRIPT_NAME} || '';
    return $sn ne '' ? $sn : 'csf.cgi';
}

sub _tail_cmd {
    my ($file, $lines) = @_;
    $lines =~ s/\D//g; $lines = $DEFAULT_LINES if !$lines || $lines == 0;

    # În CSF, %config e disponibil global
    my $tail = $main::config{TAIL} // '/usr/bin/tail';
    return "Executable [$tail] invalid\n" unless -x $tail;
    return "<---- $file is currently empty ---->\n" if -z $file;

    my $timeout = 30;
    my ($childin, $childout, $childerr) = (undef, undef, gensym);
    my $pid = eval { open3($childin, $childout, $childerr, $tail, "-$lines", $file) };
    if ($@) { return "ERROR: cannot exec tail: $@\n" }

    my $out = '';
    eval {
        local $SIG{ALRM} = sub { die "TIMEOUT\n" };
        alarm($timeout);
        while (my $line = <$childout>) { $out .= _e($line) }
        alarm(0);
        waitpid($pid, 0);
    };
    if ($@) { $out = "TIMEOUT: tail command took too long. Timed out after $timeout seconds\n" }
    return $out;
}

sub _gather_logs {
    # Emulează comportamentul clasic: citește /etc/csf/csf.syslogs + Include
    my @data = _slurp('/etc/csf/csf.syslogs');
    for my $line (@data) {
        if ($line =~ /^Include\s*(.*)$/) {
            my $inc = $1; $inc =~ s/\s+$//;
            push @data, _slurp($inc);
        }
    }
    my @out;
    for my $file (@data) {
        $file =~ s/^\s+|\s+$//g;
        next if $file eq '' || $file =~ /^\s*\#/ || $file =~ /Include/;
        my @globfiles;
        if ($file =~ /[\*\?\[]/) { push @globfiles, glob $file }
        else                     { push @globfiles, $file }

        for my $gf (@globfiles) {
            next unless -f $gf;
            push @out, $gf;
        }
    }
    # unic + sort
    my %seen; @out = grep { !$seen{$_}++ } @out;
    @out = sort @out;
    return @out;
}

# ===== UI render =====
sub _render_ui {
    my $q = CGI->new;
    my $script = _script_url();                 # păstrează /cpsess… dacă există
    my $ping_url = "$script?action=logtail&op=ping";
    my $tail_url = "$script?action=logtail&op=tail";

    my @logs = _gather_logs();
    my $options = "<select id='CSFlognum' onchange='CSFrefreshtimer()'>\n";
    my $cnt = 0;
    for my $gf (@logs) {
        my $size_kb = -f $gf ? int((stat($gf)->size)/1024) : 0;
        $options .= "<option value='$cnt'";
        $options .= " selected" if $gf eq '/var/log/lfd.log';
        my $label = _e("$gf ($size_kb kb)");
        $options .= ">$label</option>\n";
        $cnt++;
    }
    $options .= "</select>\n";

    # Include js clasic dacă există (altfel folosim un JS minimal inline)
    my @js = _slurp('/usr/local/csf/lib/csfajaxtail.js');
    my $has_external = scalar @js ? 1 : 0;

    _header_html();
    print <<"HTML";
<div>$options Lines:<input type='text' id="CSFlines" value="$DEFAULT_LINES" size='4'>
&nbsp;&nbsp;<button class='btn btn-default' onclick="CSFrefreshtimer()">Refresh Now</button></div>
<div>Refresh in <span id="CSFtimer">0</span>
<button class='btn btn-default' id="CSFpauseID" onclick="CSFpausetimer()" style="width:80px;">Pause</button>
<img src="/images/loader.gif" id="CSFrefreshing" style="display:none" /></div>
<div class='pull-right btn-group'>
  <button class='btn btn-default' id='fontminus-btn'><strong>a</strong></button>
  <button class='btn btn-default' id='fontplus-btn'><strong>A</strong></button>
</div>
<pre class='comment' id="CSFajax" style="overflow:auto;height:500px;resize:both; white-space: pre-wrap;clear:both"> &nbsp; </pre>

<script>
  // endpoint-uri
  var CSFping   = '$ping_url';
  var CSFscript = '$tail_url'; // endpoint pentru tail
  var CSFfrombot = 120, CSFfromright = 10;

  // font size controls
  var myFont = 14;
  document.getElementById('fontplus-btn').addEventListener('click', function () {
    myFont = Math.min(20, myFont+1);
    document.getElementById('CSFajax').style.fontSize = myFont+'px';
  });
  document.getElementById('fontminus-btn').addEventListener('click', function () {
    myFont = Math.max(12, myFont-1);
    document.getElementById('CSFajax').style.fontSize = myFont+'px';
  });

  // JS minimal dacă nu există csfajaxtail.js
  if (!($has_external)) {
    window.CSFtimer = function(){
      // no-op basic; refresh imediat
      window.CSFrefreshtimer();
    };
    window.CSFpausetimer = function(){ /* no-op */ };
    window.CSFrefreshtimer = function(){
      var sel   = document.getElementById('CSFlognum');
      var lines = document.getElementById('CSFlines').value.replace(/\\D/g,'') || $DEFAULT_LINES;
      var url = CSFscript + '&lognum=' + encodeURIComponent(sel.value) + '&lines=' + encodeURIComponent(lines);
      document.getElementById('CSFrefreshing').style.display = '';
      fetch(url, {credentials:'same-origin'}).then(r=>r.text()).then(function(t){
        document.getElementById('CSFajax').textContent = t;
      }).catch(function(e){
        document.getElementById('CSFajax').textContent = 'ERROR: ' + e;
      }).finally(function(){
        document.getElementById('CSFrefreshing').style.display = 'none';
      });
    };
  }
  // pornește timerul inițial
  if (window.CSFtimer) CSFtimer();
</script>
HTML

    # dacă există js clasic, îl injectăm ca <script>
    if ($has_external) {
        print "<script>\n", @js, "\n</script>\n";
        print "<script> if (window.CSFtimer) CSFtimer(); </script>\n";
    }
    return;
}

# ===== API ops =====
sub _op_ping {
    _header_json();
    print encode_json($PING_OK);
    return;
}

sub _op_list {
    _header_json();
    my @logs = _gather_logs();
    my @payload;
    for my $i (0..$#logs) {
        my $f = $logs[$i];
        my $size_kb = -f $f ? int((stat($f)->size)/1024) : 0;
        push @payload, { idx=>$i, file=>$f, kb=>$size_kb };
    }
    print encode_json({ logs => \@payload });
    return;
}

sub _op_tail {
    my $q = CGI->new;
    my $lines = $q->param('lines') // $DEFAULT_LINES;
    my $idx   = $q->param('lognum') // 0;

    my @logs = _gather_logs();
    $idx =~ s/\D//g; $idx = 0 if !defined $idx || $idx > $#logs;
    my $file = $logs[$idx] // '/var/log/lfd.log';

    # livrăm text/plain doar dacă nu suntem embedded; altfel fără header
    unless ($EMBEDDED) {
        my $qq = CGI->new; print $qq->header(-type=>'text/plain', -charset=>'utf-8');
    }
    print _tail_cmd($file, $lines);
    return;
}

# ===== Entry point =====
sub terminal {
    my $q = CGI->new;
    my $op = $q->param('op') // '';

    if    ($op eq 'ping') { return _op_ping(); }
    elsif ($op eq 'list') { return _op_list(); }
    elsif ($op eq 'tail') { return _op_tail(); }
    else                  { return _render_ui(); }
}

1;
