package Gniza4cpCPanel::UI;
# Shared UI helpers for the gniza4cp cPanel user restore plugin.
# Adapted from Gniza4cpWHM::UI for cPanel context (runs as user, not root).

use strict;
use warnings;
use Fcntl qw(:DEFAULT);

my $CSS_FILE  = '/usr/local/cpanel/base/frontend/jupiter/gniza4cp/assets/gniza4cp-whm.css';
my $LOGO_FILE = '/usr/local/cpanel/base/frontend/jupiter/gniza4cp/assets/gniza4cp-logo.svg';
my $_logo_data_uri = '';

my @NAV_ITEMS = (
    { url => 'index.live.cgi',   label => 'Restore' },
    { url => 'logs.live.cgi',    label => 'Logs' },
);

# ── HTML Escaping ─────────────────────────────────────────────

sub esc {
    my ($str) = @_;
    $str //= '';
    $str =~ s/&/&amp;/g;
    $str =~ s/</&lt;/g;
    $str =~ s/>/&gt;/g;
    $str =~ s/"/&quot;/g;
    $str =~ s/'/&#39;/g;
    return $str;
}

# ── Navigation ────────────────────────────────────────────────

sub render_nav {
    my ($current_page) = @_;
    my $logo = '';
    if ($_logo_data_uri) {
        $logo = qq{<img src="$_logo_data_uri" alt="" style="height:3rem;width:auto">}
              . qq{<span class="text-3xl font-bold leading-none">GNIZA4CP <span class="text-secondary">Backup</span></span>};
    }
    my $menu_items = '';
    for my $item (@NAV_ITEMS) {
        my $is_active = ($item->{url} eq $current_page);
        my $label = esc($item->{label});
        my $style = $is_active
            ? 'style="font-weight:600;color:inherit"'
            : 'style="color:inherit;opacity:0.7"';
        $menu_items .= qq{<a class="no-underline" href="$item->{url}" $style>$label</a>\n};
    }

    # Use inline styles to avoid cPanel Jupiter CSS conflicts with DaisyUI navbar
    my $html = qq{<div class="bg-base-200 rounded-box mb-5" style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 1rem;min-height:4rem">\n};
    $html .= qq{  <div style="display:flex;align-items:center;gap:0.5rem">\n};
    $html .= qq{    $logo\n} if $logo;
    $html .= qq{  </div>\n};
    $html .= qq{  <div style="display:flex;align-items:center;gap:1.5rem;font-size:0.95rem">\n};
    $html .= qq{    $menu_items};
    $html .= qq{  </div>\n};
    $html .= qq{</div>\n};
    return $html;
}

# ── Current User ─────────────────────────────────────────────

sub get_current_user {
    return $ENV{'REMOTE_USER'} // '';
}

# ── Safe File I/O (symlink-safe) ──────────────────────────────

sub _safe_write {
    my ($file, $content) = @_;
    # Remove existing file/symlink before writing (prevents symlink attacks)
    unlink $file if -e $file || -l $file;
    # Create new file with O_CREAT|O_EXCL (fails if file already exists/race)
    if (sysopen my $fh, $file, O_WRONLY | O_CREAT | O_EXCL, 0600) {
        print $fh $content;
        close $fh;
        return 1;
    }
    return 0;
}

sub _safe_read {
    my ($file) = @_;
    # Refuse to read symlinks
    return if -l $file;
    return unless -f $file;
    if (open my $fh, '<', $file) {
        local $/;
        my $content = <$fh>;
        close $fh;
        return $content;
    }
    return;
}

# ── Flash Messages ────────────────────────────────────────────

sub _flash_file {
    my $user = get_current_user();
    return "/tmp/.gniza4cp-cpanel-flash-$user";
}

sub set_flash {
    my ($type, $text) = @_;
    _safe_write(_flash_file(), "$type\n$text\n");
    return;
}

sub get_flash {
    my $file = _flash_file();
    my $content = _safe_read($file);
    return unless defined $content;
    unlink $file;

    my ($type, $text) = split /\n/, $content, 2;

    return unless defined $type && defined $text;
    chomp $type;
    chomp $text;
    return ($type, $text);
}

sub render_flash {
    my @flash = get_flash();
    return '' unless defined $flash[0];
    my ($type, $text) = @flash;
    # Validate flash type against allowlist
    my %valid_types = map { $_ => 1 } qw(success error info warning);
    $type = 'info' unless $valid_types{$type // ''};
    my $escaped = esc($text);
    return qq{<div class="alert alert-$type mb-4">$escaped</div>\n};
}

# ── CSRF Protection ──────────────────────────────────────────

my $_current_csrf_token;

sub _csrf_file {
    my $user = get_current_user();
    return "/tmp/.gniza4cp-cpanel-csrf-$user";
}

sub generate_csrf_token {
    return $_current_csrf_token if defined $_current_csrf_token;

    my $token = '';
    if (open my $ufh, '<:raw', '/dev/urandom') {
        my $bytes;
        read $ufh, $bytes, 32;
        close $ufh;
        $token = unpack('H*', $bytes);
    } else {
        # Fallback (should not happen on Linux)
        for (1..32) {
            $token .= sprintf('%02x', int(rand(256)));
        }
    }

    my $csrf_file = _csrf_file();
    unless (_safe_write($csrf_file, time() . "\n" . $token . "\n")) {
        if (open my $fh, '>', $csrf_file) {
            print $fh time() . "\n" . $token . "\n";
            close $fh;
        }
    }

    $_current_csrf_token = $token;
    return $token;
}

sub verify_csrf_token {
    my ($submitted) = @_;
    return 0 unless defined $submitted && $submitted ne '';

    my $file = _csrf_file();
    my $content = _safe_read($file);
    return 0 unless defined $content;

    my ($stored_time, $stored_token) = split /\n/, $content, 2;
    return 0 unless defined $stored_time && defined $stored_token;

    chomp $stored_time;
    chomp $stored_token;

    # Delete after use (single-use)
    unlink $file;

    # Check expiry (1 hour)
    return 0 if (time() - $stored_time) > 3600;

    # Constant-time comparison
    return 0 if length($submitted) != length($stored_token);
    my $result = 0;
    for my $i (0 .. length($submitted) - 1) {
        $result |= ord(substr($submitted, $i, 1)) ^ ord(substr($stored_token, $i, 1));
    }
    return $result == 0;
}

sub csrf_hidden_field {
    my $token = generate_csrf_token();
    return qq{<input type="hidden" name="gniza4cp_csrf" value="} . esc($token) . qq{">};
}

# ── Page Wrappers ────────────────────────────────────────────

sub page_header {
    my ($title) = @_;
    $title = esc($title // 'gniza4cp Restore');
    my $css = '';
    if (open my $fh, '<', $CSS_FILE) {
        local $/;
        $css = <$fh>;
        close $fh;
    }
    $css = _unwrap_layers($css);
    $css = _scope_to_container($css);

    # Pre-compute logo data URI for render_nav()
    if (!$_logo_data_uri && open my $lfh, '<', $LOGO_FILE) {
        local $/;
        my $svg_data = <$lfh>;
        close $lfh;
        require MIME::Base64;
        $_logo_data_uri = 'data:image/svg+xml;base64,' . MIME::Base64::encode_base64($svg_data, '');
    }

    return qq{<style>$css</style>\n}
         . qq{<div data-theme="gniza4cp" class="font-sans text-base" style="padding:20px 10px 10px 10px">\n};
}

sub page_footer {
    return qq{</div>\n};
}

sub _unwrap_layers {
    my ($css) = @_;
    while ($css =~ /\@layer\s/) {
        $css =~ s/\@layer\s+[\w.,\s]+\s*;//g;
        my $out = '';
        my $i = 0;
        my $len = length($css);
        while ($i < $len) {
            if (substr($css, $i, 6) eq '@layer') {
                my $brace = index($css, '{', $i);
                if ($brace == -1) { $out .= substr($css, $i); last; }
                my $semi = index($css, ';', $i);
                if ($semi != -1 && $semi < $brace) {
                    $i = $semi + 1;
                    next;
                }
                my $depth = 1;
                my $j = $brace + 1;
                while ($j < $len && $depth > 0) {
                    my $c = substr($css, $j, 1);
                    $depth++ if $c eq '{';
                    $depth-- if $c eq '}';
                    $j++;
                }
                $out .= substr($css, $brace + 1, $j - $brace - 2);
                $i = $j;
            } else {
                $out .= substr($css, $i, 1);
                $i++;
            }
        }
        $css = $out;
    }
    return $css;
}

sub _scope_to_container {
    my ($css) = @_;
    $css =~ s/:root,\s*:host/\&/g;
    $css =~ s/:where\(:root,\s*\[data-theme[^\]]*\]\)/\&/g;
    $css =~ s/:where\(:root\)/\&/g;
    $css =~ s/:root,\s*\[data-theme[^\]]*\]/\&/g;
    $css =~ s/\[data-theme=light\]/\&/g;
    $css =~ s/\[data-theme=gniza4cp\]/\&/g;
    $css =~ s/:root:not\(span\)/\&/g;
    $css =~ s/:root:has\(/\&:has(/g;
    $css =~ s/:root\b/\&/g;

    my @top_level;
    my $scoped = '';
    my $i = 0;
    my $len = length($css);
    while ($i < $len) {
        if (substr($css, $i, 1) eq '@') {
            if (substr($css, $i, 11) eq '@keyframes '
             || substr($css, $i, 10) eq '@property ') {
                my $brace = index($css, '{', $i);
                if ($brace == -1) { $scoped .= substr($css, $i); last; }
                my $depth = 1;
                my $j = $brace + 1;
                while ($j < $len && $depth > 0) {
                    my $c = substr($css, $j, 1);
                    $depth++ if $c eq '{';
                    $depth-- if $c eq '}';
                    $j++;
                }
                push @top_level, substr($css, $i, $j - $i);
                $i = $j;
                next;
            }
        }
        $scoped .= substr($css, $i, 1);
        $i++;
    }

    return join('', @top_level) . '[data-theme="gniza4cp"]{' . $scoped . '}';
}

sub render_errors {
    my ($errors) = @_;
    return '' unless $errors && @$errors;
    my $html = qq{<div class="alert alert-error mb-4">\n<ul class="list-disc pl-5">\n};
    for my $err (@$errors) {
        $html .= '  <li>' . esc($err) . "</li>\n";
    }
    $html .= "</ul>\n</div>\n";
    return $html;
}

1;
