#!/usr/bin/perl

use strict;
use warnings;
use Gtk2 -init;
use Cairo;
use Glib;
use POSIX qw(tan log sqrt floor strftime);
use constant PI => 3.14159265;
use Fcntl qw(SEEK_SET O_NONBLOCK F_GETFL F_SETFL);
use Net::IP;
use File::Basename;
use Text::CSV;
use Storable qw(retrieve store thaw);
use IO::Socket::INET;
use Errno qw(EAGAIN);
use DBI;

# --- Configuration ---
my $listen_port = 6789;
my $required_password = "Xplr24242";
my $home_dir = $ENV{'HOME'} or die "HOME environment variable not set";
my $resource_dir = "resources";
my $config_file = "connmapperl2rc";
my $history_file = ".connmap-history2.dat"; # For the simple history
my $db_file = ".connmap-history2.sqlite";    # For the detailed history
my $image_path = "$resource_dir/w1000-old.png";
my $ipv4_db_path = "$resource_dir/ipv4.csv";
my $geolite_file = "$resource_dir/GeoLite2-ASN-Blocks-IPv4.csv";
my $weblog_file_path = '/var/log/nginx/access.log';
my $show_close_button = 1;
my $debug = 0;
my $historytimelimit = 6; # Default: Prune history older than 6 hours
my $pruning_enabled = 1; # Default: Enable history pruning

# --- Global State ---
my $state = {
    current_points     => [],
    client_connections => {},
    history_points     => {}, # Simple history (in-memory)
    active_connections_cache => {}, # Cache to track unique, ongoing connections
    show_history       => 0,
    map_width          => 1000,
    map_height         => 500,
    dot_diameter       => 8,
    default_dot_color  => [1, 0.2, 0.2, 0.8],
    text_color         => [1, 1, 1, 0.9],
    ip2asn             => {},
    port_colors        => [],
    show_text_globally => 1,
    right_click_circle => undef,
    color_key_areas    => [],
    status_message     => "",
    status_message_timer_id => undef,
    show_help_text     => 1,
    help_text_area     => undef,
    all_text_toggle_state => 0,
    client_visibility  => {},
    show_client_list   => 1,
    client_list_areas  => [],
    client_list_close_area => undef,
    hilbert_area       => undef, 
    hilbert_pixmap     => undef, # Cached background for the Hilbert window
    hilbert_right_click_circle => undef,
    detailed_history_enabled => 1, 
};

my $window;
my $drawing_area;
my @client_sockets;
my %client_buffers;
my %client_auth_state;
my $ip2asn_lookup_enabled = 0;
my $ip2asn_script_path = "";

# --- SQLite Database Globals ---
my $dbh;
my @detailed_history_queue; # Queue for non-blocking writes
my $db_worker_busy = 0;


#======================================================================
# Database Handling
#======================================================================
sub init_database {
    my $dsn = "dbi:SQLite:dbname=$db_file";
    $dbh = DBI->connect($dsn, "", "", { RaiseError => 1, AutoCommit => 1 })
        or die "Can't connect to SQLite database: $DBI::errstr";
    
    $dbh->do("PRAGMA journal_mode = WAL;");
    
    $dbh->do(q{
        CREATE TABLE IF NOT EXISTS detailed_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp INTEGER NOT NULL,
            ip TEXT NOT NULL,
            port INTEGER NOT NULL,
            local_ip TEXT NOT NULL,
            local_port INTEGER NOT NULL,
            programname TEXT,
            full_lookup TEXT,
            asn INTEGER,
            orgname TEXT,
            lat REAL NOT NULL,
            lon REAL NOT NULL,
            x REAL, 
            y REAL,
            count INTEGER 
        )
    });

    # --- Update schema for existing databases ---
    my $sth = $dbh->prepare("PRAGMA table_info(detailed_history)");
    $sth->execute();
    my %columns;
    while (my $row = $sth->fetchrow_hashref) {
        $columns{$row->{name}} = 1;
    }
    $sth->finish();

    unless (exists $columns{asn}) {
        print "Adding 'asn' column to detailed_history table.\n";
        $dbh->do("ALTER TABLE detailed_history ADD COLUMN asn INTEGER");
    }
    unless (exists $columns{orgname}) {
        print "Adding 'orgname' column to detailed_history table.\n";
        $dbh->do("ALTER TABLE detailed_history ADD COLUMN orgname TEXT");
    }
    
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_timestamp ON detailed_history (timestamp)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_x ON detailed_history (x)");
    $dbh->do("CREATE INDEX IF NOT EXISTS idx_y ON detailed_history (y)");

    print "SQLite detailed history database initialized at '$db_file'.\n";
}

sub process_db_queue {
    return 1 if $db_worker_busy || !@detailed_history_queue;
    $db_worker_busy = 1;
    
    my @items_to_process = @detailed_history_queue;
    @detailed_history_queue = ();
    
    eval {
        my $sql = q{
            INSERT INTO detailed_history 
            (timestamp, ip, port, local_ip, local_port, programname, full_lookup, asn, orgname, lat, lon, x, y, count)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        };
        my $sth = $dbh->prepare($sql);
        
        $dbh->begin_work;
        
        for my $entry (@items_to_process) {
            $sth->execute(
                $entry->{timestamp}, $entry->{ip}, $entry->{port},
                $entry->{local_ip}, $entry->{local_port}, $entry->{programname},
                $entry->{full_lookup},
                $entry->{asn},
                $entry->{orgname},
                $entry->{lat}, $entry->{lon},
                $entry->{x}, $entry->{y}, $entry->{count}
            );
        }
        
        $dbh->commit;
        $sth->finish;
    };
    if ($@) {
        warn "Database transaction failed: $@. Rolling back.";
        eval { $dbh->rollback };
    }
    
    $db_worker_busy = 0;
    return 1;
}


#======================================================================
# Status Message Handling
#======================================================================
sub set_status_message {
    my ($message) = @_;
    print "$message\n";
    $state->{status_message} = $message;
    if (defined $state->{status_message_timer_id}) {
        Glib::Source->remove($state->{status_message_timer_id});
        $state->{status_message_timer_id} = undef;
    }
    $state->{status_message_timer_id} = Glib::Timeout->add(2000, sub {
        $state->{status_message} = "";
        $state->{status_message_timer_id} = undef;
        $drawing_area->queue_draw() if $drawing_area;
        return 0;
    });
    $drawing_area->queue_draw() if $drawing_area;
}

#======================================================================
# Configuration & History I/O
#======================================================================
sub save_history_points {
    eval { store($state->{history_points}, $history_file); };
    if ($@) {
        set_status_message("Error saving simple history: $@");
        return;
    }
    set_status_message("Simple history saved to $history_file");
}

sub load_history_points {
    unless (-e $history_file) {
        set_status_message("Simple history file not found: $history_file");
        return;
    }
    my $loaded_history;
    eval { $loaded_history = retrieve($history_file); };
    if ($@) {
        set_status_message("Error loading simple history: $@");
        return;
    }
    unless (ref($loaded_history) eq 'HASH') {
        set_status_message("Error: Simple history file is corrupt or not valid.");
        return;
    }
    $state->{history_points} = $loaded_history;
    set_status_message("Simple history loaded from $history_file");
    $drawing_area->queue_draw();
}

sub load_port_colors_from_config {
    my ($file) = @_;
    unless (-e $file) {
        warn "Configuration file not found: $file. Using default colors only.\n";
        return;
    }
    my $csv = Text::CSV->new({ binary => 1, auto_diag => 1, allow_loose_quotes => 1 });
    open(my $fh, "<:encoding(utf8)", $file) or do {
        warn "Could not open config file '$file': $!. Using defaults.\n";
        return;
    };
    my @loaded_colors;
    while (my $row = $csv->getline($fh)) {
        next if $row->[0] =~ /^\s*#/;
        next unless @$row == 8;
        my ($label, $type, $port_or_app, $r, $g, $b, $a, $show_text) = @$row;
        unless (defined $label && ($type eq 'local' || $type eq 'peer' || $type eq 'app') && defined $show_text) {
            warn "Skipping invalid line in config file: " . join(',', @$row) . "\n";
            next;
        }
        push @loaded_colors, {
            label => $label, type => $type, target => $port_or_app,
            color => [$r, $g, $b, $a], show_text => int($show_text),
        };
    }
    close $fh;
    $state->{port_colors} = \@loaded_colors;
    print "Loaded " . scalar(@loaded_colors) . " color rules from $config_file.\n";
}

#======================================================================
# IP Geolocation and Coordinate Functions
#======================================================================
sub ip_to_decimal {
    my ($ip_str) = @_;
    my @parts = split /\./, $ip_str;
    return 0 unless @parts == 4;
    return ($parts[0] << 24) + ($parts[1] << 16) + ($parts[2] << 8) + $parts[3];
}

sub find_location_on_disk {
    my ($fh, $ip_decimal, $file_size) = @_;
    my ($low_pos, $high_pos) = (0, $file_size);
    for (1..30) {
        last if ($high_pos - $low_pos) < 2;
        my $mid_pos = int($low_pos + ($high_pos - $low_pos) / 2);
        seek($fh, $mid_pos, 0); <$fh>;
        my $line = <$fh>;
        return undef unless defined $line;
        my ($start, $end, $lat, $lon) = split ',', $line;
        next unless defined $start and defined $end;
        if ($ip_decimal >= $start && $ip_decimal <= $end) {
            return { lat => $lat, lon => $lon };
        }
        $ip_decimal < $start ? ($high_pos = $mid_pos) : ($low_pos = $mid_pos);
    }
    return undef;
}

sub latlon_to_xy {
    my ($latitude, $longitude) = @_;
    chomp($latitude);
    chomp($longitude);
    $latitude = 0 unless $latitude;
    $longitude = 0 unless $longitude;
    my ($width, $height) = ($state->{map_width}, $state->{map_height});
    my ($xOffset, $yOffset) = (-29.0, 76.0);
    my $x = ($longitude + 180) * ($width / 360) + $xOffset;
    $x -= (($longitude/180) * 15) if $longitude > 90;
    my $latRad = $latitude * PI / 180;
    my $mercN = log(tan((PI / 4) + ($latRad / 2)));
    my $y = ($height / 2) - ($width * $mercN / (2 * PI)) + $yOffset;
    return ($x, $y);
}

sub find_ip_in_geolite_asn {
    my ($geolite_csv_file, $ip_to_check) = @_;
    my $input_ip = Net::IP->new($ip_to_check);
    return unless $input_ip && $input_ip->version() == 4;
    my $input_ip_num = $input_ip->intip();
    open(my $fh, '<', $geolite_csv_file) or return;
    my $file_size = -s $fh;
    my ($low, $high) = (0, $file_size);
    my $found_line;
    $low = length(scalar <$fh>); # Skip header
    while ($low <= $high) {
        my $mid = int(($low + $high) / 2);
        seek($fh, $mid, SEEK_SET); <$fh> if $mid > 0;
        my $line = <$fh>;
        next unless defined $line && $line =~ /\S/;
        my ($network_cidr) = split /,/, $line, 2;
        my $network_ip = Net::IP->new($network_cidr);
        unless ($network_ip) { $high = $mid -1; next; }
        my $start_ip_num = $network_ip->intip();
        my $end_ip_num   = $start_ip_num + ($network_ip->size() - 1);
        if ($input_ip_num >= $start_ip_num && $input_ip_num <= $end_ip_num) {
            $found_line = $line; last;
        } elsif ($input_ip_num < $start_ip_num) {
            $high = $mid - 1;
        } else { $low = $mid + 1; }
    }
    close $fh;
    return unless defined $found_line;
    my @fields = split /,/, $found_line;
    return { network => $fields[0], asn => $fields[1], organization => $fields[2] || "N/A" };
}

sub get_point_label_text {
    my ($point) = @_;
    my $programname = $point->{programname} // "";
    my $remoteclientip = $point->{local_ip} // "local";
    if ($state->{ip2asn}->{$point->{ip}}) {
        return "$remoteclientip:$programname:$point->{local_port} -> $point->{ip}:$point->{port} - $state->{ip2asn}->{$point->{ip}}";
    } else {
        my $asn_hash = find_ip_in_geolite_asn($geolite_file, $point->{ip});
        if ($asn_hash) {
            my $asn_org_str = "$asn_hash->{asn} - $asn_hash->{organization}";
            $state->{ip2asn}->{$point->{ip}} = $asn_org_str;
            return "$remoteclientip:$programname:$point->{local_port} -> $point->{ip}:$point->{port} - $asn_org_str";
        } else {
            return "$remoteclientip:$programname:$point->{local_port} -> $point->{ip}:$point->{port}";
        }
    }
}

sub get_ip_logs {
    my ($ipaddress, $logfile) = @_;
    unless ($ipaddress =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
        warn "Invalid IP address format for log search: $ipaddress\n";
        return ("Invalid IP address format: $ipaddress");
    }
    my @lines = `grep '^$ipaddress' $logfile`;
    return @lines;
}

#======================================================================
# History Pruning
#======================================================================
sub prune_history {
    return 1 unless $pruning_enabled;

    my $now = time();
    my $cutoff = $now - ($historytimelimit * 3600);
    my $pruned_count = 0;

    eval {
        my $sth = $dbh->prepare("DELETE FROM detailed_history WHERE timestamp < ?");
        $pruned_count = $sth->execute($cutoff);
        $sth->finish();
        $dbh->do("VACUUM") if $pruned_count > 0;
    };
    if ($@) {
        warn "SQLite history pruning failed: $@";
    }

    if ($pruned_count > 0) {
        print "Pruned $pruned_count old entries. Rebuilding simple history cache...\n";
        
        my %new_history_points;
        eval {
            my $sth = $dbh->prepare("SELECT lat, lon, COUNT(*) FROM detailed_history GROUP BY lat, lon");
            $sth->execute();
            while (my ($lat, $lon, $count) = $sth->fetchrow_array()) {
                next unless defined $lat && defined $lon;
                my $key = "$lat,$lon";
                $new_history_points{$key} = {
                    count => $count,
                    show_text_seen => 1 
                };
            }
            $sth->finish();
        };
        if ($@) {
            warn "Failed to rebuild simple history from DB: $@";
            return 1;
        }

        $state->{history_points} = \%new_history_points;

        $drawing_area->queue_draw() if $drawing_area;
        set_status_message("History pruned and display updated.");
    }

    return 1;
}


#======================================================================
# GTK, Drawing, and Timer Functions
#======================================================================
sub process_and_draw {
    my ($drawing_area) = @_;
    open my $db_fh, '<', $ipv4_db_path or return 1;
    my $db_size = -s $db_fh;
    
    my @connections;
    for my $fileno (keys %{$state->{client_connections}}) {
        push @connections, @{ $state->{client_connections}->{$fileno} || [] };
    }
    
    my @new_points;
    my %current_snapshot_keys;

    for my $conn (@connections) {
        my $ip_decimal = ip_to_decimal($conn->{ip});
        next unless $ip_decimal;
        my $location = find_location_on_disk($db_fh, $ip_decimal, $db_size);
        next unless $location; 

        $conn->{lat} = $location->{lat};
        $conn->{lon} = $location->{lon};
        push @new_points, $conn;
        
        my $conn_key = "$conn->{local_ip}:$conn->{local_port}:$conn->{ip}:$conn->{port}";
        $current_snapshot_keys{$conn_key} = 1;

        unless (exists $state->{active_connections_cache}->{$conn_key}) {
            $state->{active_connections_cache}->{$conn_key} = 1; 

            my $history_key = "$location->{lat},$location->{lon}";
            unless (exists $state->{history_points}->{$history_key}) {
                $state->{history_points}->{$history_key} = { count => 0, show_text_seen => 0 };
            }
            $state->{history_points}->{$history_key}->{count}++;
            $state->{history_points}->{$history_key}->{show_text_seen} = 1;

            if ($state->{detailed_history_enabled}) {
                my $full_lookup = get_point_label_text($conn);

                # --- START: Parse ASN and orgname from full_lookup string ---
                my ($asn, $orgname) = (undef, undef);
                if ($full_lookup =~ / - (\d+)\s+-\s+(.+)$/) {
                    $asn = $1;
                    $orgname = $2;
                    $orgname =~ s/\s+$//; # Clean trailing space
                }
                # --- END: Parsing logic ---

                my ($x, $y) = latlon_to_xy($location->{lat}, $location->{lon});
                
                push @detailed_history_queue, {
                    timestamp   => time(),
                    ip          => $conn->{ip},
                    port        => $conn->{port},
                    local_ip    => $conn->{local_ip},
                    local_port  => $conn->{local_port},
                    programname => $conn->{programname},
                    full_lookup => $full_lookup,
                    asn         => $asn,
                    orgname     => $orgname,
                    lat         => $location->{lat},
                    lon         => $location->{lon},
                    x           => $x,
                    y           => $y,
                    count       => $state->{history_points}->{$history_key}->{count},
                };
            }
        }
    }

    for my $cached_key (keys %{$state->{active_connections_cache}}) {
        unless (exists $current_snapshot_keys{$cached_key}) {
            delete $state->{active_connections_cache}->{$cached_key};
        }
    }

    close $db_fh;
    $state->{current_points} = \@new_points;
    
    $drawing_area->queue_draw();
    $state->{hilbert_area}->queue_draw() if $state->{hilbert_area};
    
    return 1;
}

sub draw_color_key {
    my ($cr, $points_by_rule) = @_;
    my @key_items;
    my %seen_labels;

    for my $rule (@{$state->{port_colors}}) {
        my $label = $rule->{label};
        unless ($seen_labels{$label}) {
            push @key_items, { label => $label, color => $rule->{color} };
            $seen_labels{$label} = 1;
        }
    }
    push @key_items, { label => "Other", color => $state->{default_dot_color} };

    my $x_start = 15;
    my $line_height = 20;
    my $y_start = $state->{map_height} - (scalar(@key_items) * $line_height) - 10;
    my $box_size = 10;

    $cr->select_font_face('Sans', 'normal', 'bold');
    $cr->set_font_size(10);
    
    $cr->set_source_rgba(@{$state->{text_color}});
    $cr->move_to($x_start, $y_start - 5);
    $cr->show_text("Total Conns: " . scalar(@{$state->{current_points}}));

    my $current_y = $y_start;
    my @key_areas;

    for my $item (@key_items) {
        my $label_text = $item->{label};
        my $points_ref = $points_by_rule->{$label_text} || [];
        my $count = scalar(@$points_ref);
        my $display_text = "$label_text ($count)";

        my $extents = $cr->text_extents($display_text);
        
        push @key_areas, {
            x_box => $x_start, 
            y_box => $current_y, 
            w_box => $box_size, 
            h_box => $box_size,
            x_text => $x_start + 18,
            y_text => $current_y,
            w_text => $extents->{width} + 5,
            h_text => $line_height,
            label => $label_text,
            points_ref => $points_ref,
        };

        $cr->set_source_rgba(@{$item->{color}});
        $cr->rectangle($x_start, $current_y, $box_size, $box_size);
        $cr->fill;

        $cr->set_source_rgba(@{$state->{text_color}});
        $cr->move_to($x_start + 18, $current_y + 10);
        $cr->show_text($display_text);

        $current_y += $line_height;
    }
    $state->{color_key_areas} = \@key_areas;
}

sub on_draw {
    my ($widget, $event) = @_;
    my $pixbuf = $widget->{_pixbuf_};
    return 1 unless $pixbuf;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    $cr->set_source_pixbuf($pixbuf, 0, 0); $cr->paint;

    if ($state->{show_history}) {
        my $base_history_radius = $state->{dot_diameter} / 3;
        my $max_history_radius = $state->{dot_diameter} * 1.5;
        my $history_color = [1.0, 1.0, 1.0, 0.6];
        
        $cr->set_source_rgba(@{$history_color});

        for my $key (keys %{$state->{history_points}}) {
            my $history_entry = $state->{history_points}->{$key};
            next unless $history_entry->{show_text_seen};
            my ($lat, $lon) = split ',', $key;
            next unless defined $lat && defined $lon;
            my $count = $history_entry->{count};
            my $scaled_radius = $base_history_radius + log($count);
            $scaled_radius = $max_history_radius if $scaled_radius > $max_history_radius;
            my ($x, $y) = latlon_to_xy($lat, $lon);
            $cr->arc($x, $y, $scaled_radius, 0, 2 * PI);
            $cr->fill;
        }
    }

    my %points_by_rule;
    my $radius = $state->{dot_diameter} / 2;
    for my $point (@{$state->{current_points}}) {
        next unless ($state->{client_visibility}->{$point->{local_ip}} // 1);

        my ($x, $y) = latlon_to_xy($point->{lat}, $point->{lon});
        
        my ($dot_color, $show_label) = ($state->{default_dot_color}, 1);
        my $rule_matched = 0;
        for my $rule (@{$state->{port_colors}}) {
             my $match = 0;
            if ($rule->{type} eq 'app' && $point->{programname} eq $rule->{target}) {
                $match = 1;
            } elsif ($rule->{type} eq 'local' || $rule->{type} eq 'peer') {
                my $port_to_check = ($rule->{type} eq 'local') ? $point->{local_port} : $point->{port};
                if ($rule->{target} =~ /^(\d+)-(\d+)$/) {
                    if ($port_to_check >= $1 && $port_to_check <= $2) {
                        $match = 1;
                    }
                } elsif ($port_to_check == $rule->{target}) {
                    $match = 1;
                }
            }

            if ($match) {
                $dot_color = $rule->{color};
                $show_label = $rule->{show_text};
                push @{$points_by_rule{$rule->{label}}}, $point;
                $rule_matched = 1;
                last;
            }
        }
        push @{$points_by_rule{'Other'}}, $point unless $rule_matched;

        $cr->set_source_rgba(@{$dot_color});
        $cr->arc($x, $y, $radius, 0, 2 * PI); $cr->fill;
        if ($show_label && $state->{show_text_globally}) {
            $cr->set_source_rgba(@{$state->{text_color}});
            $cr->select_font_face('Sans', 'normal', 'normal');
            $cr->set_font_size(8);
            $cr->move_to($x + $radius + 2, $y + $radius / 2);
            my $label = get_point_label_text($point);
            $cr->show_text($label);
        }
    }
    draw_color_key($cr, \%points_by_rule);

    if (defined $state->{right_click_circle}) {
        my $circle = $state->{right_click_circle};
        $cr->new_path; 
        $cr->set_source_rgba(1, 1, 1, 0.9);
        $cr->set_line_width(1.5);
        $cr->arc($circle->{x}, $circle->{y}, $circle->{radius}, 0, 2 * PI);
        $cr->stroke;
    }

    if (defined $state->{status_message} && $state->{status_message} ne "") {
        my $message = $state->{status_message};
        $cr->select_font_face('Sans', 'normal', 'bold');
        $cr->set_font_size(14);
        my $extents = $cr->text_extents($message);
        my $padding = 10;
        my $rect_w = $extents->{width} + (2 * $padding);
        my $rect_h = $extents->{height} + (2 * $padding);
        my $rect_x = ($state->{map_width} - $rect_w) / 2;
        my $rect_y = 5; 
        $cr->set_source_rgba(0, 0, 0, 0.7);
        $cr->rectangle($rect_x, $rect_y, $rect_w, $rect_h);
        $cr->fill;
        my $text_x = $rect_x + $padding;
        my $text_y = $rect_y + $padding + $extents->{y_bearing} * -1;
        $cr->move_to($text_x, $text_y);
        $cr->set_source_rgba(@{$state->{text_color}});
        $cr->show_text($message);
    }
    
    my $font_size = 9;
    my $line_height = $font_size + 3;
    my $padding = 5;
    my $current_y_pos = $state->{map_height} - $padding;

    if ($state->{show_help_text}) {
        my $help_text = "q: sql query|c: reload|a: text|i: clients|k: clients|p: prune|t: text|h: history|s: save|l: load|r: reset|z: hilbert|esc: quit [close help]";
        $cr->select_font_face('Sans', 'normal', 'normal');
        $cr->set_font_size($font_size);
        my $help_extents = $cr->text_extents($help_text);
        
        $state->{help_text_area} = {
            x => $state->{map_width} - $help_extents->{width} - $padding,
            y => $current_y_pos - $line_height,
            w => $help_extents->{width},
            h => $line_height,
        };

        $cr->set_source_rgba(@{$state->{text_color}});
        $cr->move_to($state->{help_text_area}->{x}, $current_y_pos - $padding + 2);
        $cr->show_text($help_text);
        
        $current_y_pos -= ($line_height + $padding);
    }
    
    if ($state->{show_client_list}) {
        my @client_ips = sort keys %{$state->{client_visibility}};
        if (@client_ips) {
            $cr->select_font_face('Sans', 'normal', 'normal');
            $cr->set_font_size($font_size);
            
            my @list_areas;
            my $row_y = $current_y_pos - $line_height;
            my $current_x_pos = $state->{map_width} - $padding;
            
            my $close_text = "[close client list]";
            my $close_extents = $cr->text_extents($close_text);
            $current_x_pos -= $close_extents->{width};
            $state->{client_list_close_area} = { x => $current_x_pos, y => $row_y, w => $close_extents->{width}, h => $line_height };
            $cr->set_source_rgba(@{$state->{text_color}});
            $cr->move_to($current_x_pos, $row_y + $line_height - $padding + 2);
            $cr->show_text($close_text);
            
            $current_x_pos -= ($padding * 2);

            for my $ip (reverse @client_ips) {
                my $extents = $cr->text_extents($ip);
                $current_x_pos -= $extents->{width};
                
                push @list_areas, { x => $current_x_pos, y => $row_y, w => $extents->{width}, h => $line_height, ip => $ip };
                
                $cr->set_source_rgba( $state->{client_visibility}->{$ip} ? @{$state->{text_color}} : (0.7, 0.7, 0.7, 0.8) );
                $cr->move_to($current_x_pos, $row_y + $line_height - $padding + 2);
                $cr->show_text($ip);

                $current_x_pos -= ($padding * 2);
            }
            $state->{client_list_areas} = \@list_areas;
        }
    }

    return 1;
}

#======================================================================
# Hilbert Curve Visualization
#======================================================================

sub hsv_to_rgb {
    my ($h, $s, $v) = @_;
    my ($r, $g, $b);
    my $i = floor($h * 6);
    my $f = $h * 6 - $i;
    my $p = $v * (1 - $s);
    my $q = $v * (1 - $f * $s);
    my $t = $v * (1 - (1 - $f) * $s);
    $i = $i % 6;
    if ($i == 0) { ($r, $g, $b) = ($v, $t, $p); }
    elsif ($i == 1) { ($r, $g, $b) = ($q, $v, $p); }
    elsif ($i == 2) { ($r, $g, $b) = ($p, $v, $t); }
    elsif ($i == 3) { ($r, $g, $b) = ($p, $q, $v); }
    elsif ($i == 4) { ($r, $g, $b) = ($t, $p, $v); }
    else { ($r, $g, $b) = ($v, $p, $q); }
    return [$r, $g, $b];
}

sub get_color_for_octet {
    my ($octet) = @_;
    my $hue = $octet / 255;
    return hsv_to_rgb($hue, 0.9, 0.95);
}

sub _hilbert_rot {
    my ($n, $x_ref, $y_ref, $rx, $ry) = @_;
    if ($ry == 0) {
        if ($rx == 1) {
            $$x_ref = ($n - 1) - $$x_ref;
            $$y_ref = ($n - 1) - $$y_ref;
        }
        my $t = $$x_ref;
        $$x_ref = $$y_ref;
        $$y_ref = $t;
    }
}

sub _d2xy_generic {
    my ($order, $d) = @_;
    my ($x, $y) = (0, 0);
    my $s = 1;
    while ($s < 2**$order) {
        my $rx = 1 & int($d / 2);
        my $ry = 1 & ($d ^ $rx);
        _hilbert_rot($s, \$x, \$y, $rx, $ry);
        $x += $s * $rx;
        $y += $s * $ry;
        $d = int($d / 4);
        $s *= 2;
    }
    return ($x, $y);
}

sub ip_decimal_to_hilbert_xy {
    my ($ip_decimal) = @_;
    return _d2xy_generic(16, $ip_decimal);
}

sub pre_render_hilbert_background {
    my ($pixmap) = @_;
    my ($win_width, $win_height) = $pixmap->get_size;
    my $cr = Gtk2::Gdk::Cairo::Context->create($pixmap);

    my $margin = 5;
    my $key_width = 80;
    my $padding = 10;
    my $curve_area_size = $win_height - (2 * $margin);
    my $curve_x_offset = $key_width + $padding + $margin;
    my $curve_y_offset = $margin;

    $cr->set_source_rgb(0, 0, 0);
    $cr->paint;

    my $key_bar_width = 20;
    for my $y (0 .. $curve_area_size - 1) {
        my $octet = ($y / ($curve_area_size - 1)) * 255;
        my $color = get_color_for_octet($octet);
        $cr->set_source_rgb(@$color);
        $cr->rectangle($padding, $y + $curve_y_offset, $key_bar_width, 1);
        $cr->fill;
    }
    $cr->set_source_rgb(1, 1, 1);
    $cr->select_font_face('Sans', 'normal', 'normal');
    $cr->set_font_size(10);
    for my $octet ( (map {$_ * 16} 0..15), 255 ) {
        my $y_pos = ($octet / 255) * ($curve_area_size - 1);
        my $label = ($octet == 255) ? "255.x.x.x" : "$octet.0.0.0";
        $cr->move_to($key_bar_width + $padding + 5, $y_pos + $curve_y_offset + 4);
        $cr->show_text($label);
    }

    my $guide_order = 6;
    my $guide_grid_size = 2**$guide_order;
    my $num_guide_points = $guide_grid_size * $guide_grid_size;
    my $ip_block_size = 2**32 / $num_guide_points;
    $cr->set_line_width(1.5);
    my ($px, $py) = _d2xy_generic($guide_order, 0);
    for my $d (1 .. $num_guide_points - 1) {
        my $ip_start = ($d - 1) * $ip_block_size;
        my $first_octet = $ip_start >> 24;
        my $color = get_color_for_octet($first_octet);
        $cr->set_source_rgb(@$color);
        my ($hx, $hy) = _d2xy_generic($guide_order, $d);
        $cr->move_to(
            ($px / ($guide_grid_size - 1)) * $curve_area_size + $curve_x_offset,
            ($py / ($guide_grid_size - 1)) * $curve_area_size + $curve_y_offset
        );
        $cr->line_to(
            ($hx / ($guide_grid_size - 1)) * $curve_area_size + $curve_x_offset,
            ($hy / ($guide_grid_size - 1)) * $curve_area_size + $curve_y_offset
        );
        $cr->stroke();
        ($px, $py) = ($hx, $hy);
    }
}

sub on_hilbert_draw {
    my ($widget, $event) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    return 1 unless $state->{hilbert_pixmap};

    # 1. Blit the pre-rendered background
    $cr->set_source_pixmap($state->{hilbert_pixmap}, 0, 0);
    $cr->paint;

    # 2. Draw the dynamic elements (points and selection circle)
    my ($win_width, $win_height) = $widget->window->get_size;
    my $margin = 5;
    my $key_width = 80;
    my $padding = 10;
    my $curve_area_size = $win_height - (2 * $margin);
    my $curve_x_offset = $key_width + $padding + $margin;
    my $curve_y_offset = $margin;
    my $hilbert_grid_size = 2**16;
    my $radius = $state->{dot_diameter} / 2;

    for my $point (@{$state->{current_points}}) {
        next unless ($state->{client_visibility}->{$point->{local_ip}} // 1);

        my $ip_decimal = ip_to_decimal($point->{ip});
        my ($hx, $hy) = ip_decimal_to_hilbert_xy($ip_decimal);

        my $scaled_x = ($hx / ($hilbert_grid_size - 1)) * $curve_area_size + $curve_x_offset;
        my $scaled_y = ($hy / ($hilbert_grid_size - 1)) * $curve_area_size + $curve_y_offset;

        my $dot_color = $state->{default_dot_color};
        for my $rule (@{$state->{port_colors}}) {
            my $match = 0;
            if ($rule->{type} eq 'app' && $point->{programname} eq $rule->{target}) { $match = 1; }
            elsif ($rule->{type} eq 'local' || $rule->{type} eq 'peer') {
                my $p = ($rule->{type} eq 'local') ? $point->{local_port} : $point->{port};
                if ($rule->{target} =~ /^(\d+)-(\d+)$/) { $match = 1 if ($p >= $1 && $p <= $2); } 
                elsif ($p == $rule->{target}) { $match = 1; }
            }
            if ($match) { $dot_color = $rule->{color}; last; }
        }
        
        $cr->set_source_rgba(@{$dot_color});
        $cr->arc($scaled_x, $scaled_y, $radius, 0, 2 * PI);
        $cr->fill;
    }

    if (defined $state->{hilbert_right_click_circle}) {
        my $circle = $state->{hilbert_right_click_circle};
        $cr->new_path; 
        $cr->set_source_rgba(1, 1, 1, 0.9);
        $cr->set_line_width(1.5);
        $cr->arc($circle->{x}, $circle->{y}, $circle->{radius}, 0, 2 * PI);
        $cr->stroke;
    }
    return 1;
}

sub on_hilbert_configure {
    my ($widget, $event) = @_;
    
    # If a pixmap exists, check if the size has changed.
    if ($state->{hilbert_pixmap}) {
        my ($old_w, $old_h) = $state->{hilbert_pixmap}->get_size;
        if ($old_w == $widget->allocation->width && $old_h == $widget->allocation->height) {
            return 1; # No size change, do nothing.
        }
    }
    
    # Create a new pixmap at the new size and pre-render the background.
    my $pixmap = Gtk2::Gdk::Pixmap->new(
        $widget->window, 
        $widget->allocation->width, 
        $widget->allocation->height, 
        -1
    );
    $state->{hilbert_pixmap} = $pixmap;
    pre_render_hilbert_background($pixmap);
    
    return 1;
}

sub create_hilbert_window {
    return if $state->{hilbert_area};

    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("IPv4 Hilbert Curve");
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);

    my $curve_size = $state->{map_height};
    my $key_width = 80;
    my $padding = 10;
    my $total_width = $curve_size + $key_width + $padding * 2;
    $popup->set_default_size($total_width, $curve_size);

    my $hilbert_area = Gtk2::DrawingArea->new;
    $hilbert_area->signal_connect('expose-event' => \&on_hilbert_draw);
    $hilbert_area->signal_connect('configure-event' => \&on_hilbert_configure); # Handle resize
    $popup->add($hilbert_area);
    
    $popup->add_events(['button-press-mask']);
    $popup->signal_connect('button-press-event' => sub {
        my ($widget, $event) = @_;
        if ($event->button == 3) { # Right click
            my ($click_x, $click_y) = $event->get_coords;
            handle_hilbert_right_click($click_x, $click_y, $hilbert_area);
            return 1;
        }
        return 0;
    });

    $state->{hilbert_area} = $hilbert_area;

    $popup->signal_connect(destroy => sub {
        $state->{hilbert_area} = undef;
        $state->{hilbert_pixmap} = undef;
    });

    $popup->show_all;
}


sub create_connection_list_popup {
    my ($label, $points_ref) = @_;

    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Connections for: $label");
    $popup->set_default_size(550, 300);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(5);

    my $scrolled_window = Gtk2::ScrolledWindow->new(undef, undef);
    $scrolled_window->set_policy('automatic', 'automatic');
    $scrolled_window->set_shadow_type('in');
    
    my $textview = Gtk2::TextView->new;
    $textview->set_wrap_mode('word');
    my $buffer = $textview->get_buffer;

    my $text_content = "";
    for my $point (@$points_ref) {
        $text_content .= get_point_label_text($point) . "\n";
    }
    $buffer->set_text($text_content);

    $scrolled_window->add($textview);
    $popup->add($scrolled_window);
    $popup->show_all;
}


sub create_info_popup {
    my ($points_ref, $ip_entry_ref, $sql_query_text) = @_;
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Nearby Connections");
    $popup->set_default_size(450, 250);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(5);

    my $main_vbox = Gtk2::VBox->new(0, 5);
    $popup->add($main_vbox);

    my $lookup_hbox = Gtk2::HBox->new(0, 5);
    my $ip_label = Gtk2::Label->new("IP:");
    my $ip_entry = Gtk2::Entry->new();
    $$ip_entry_ref = $ip_entry;
    
    $lookup_hbox->pack_start($ip_label, 0, 0, 5);
    $lookup_hbox->pack_start($ip_entry, 1, 1, 0);

    if ($ip2asn_lookup_enabled) {
        my $lookup_button = Gtk2::Button->new("Look-up");
        $lookup_hbox->pack_start($lookup_button, 0, 0, 5);
        $lookup_button->signal_connect(clicked => sub {
            my $ip = $ip_entry->get_text();
            $ip =~ s/^\s+|\s+$//g;

            unless ($ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ || Net::IP->new($ip)) {
                warn "Invalid IP address format entered: '$ip'\n";
                return;
            }
            
            my $command = "perl $ip2asn_script_path \"$ip\"";
            my $output = qx($command);

            my $output_popup = Gtk2::Window->new('toplevel');
            $output_popup->set_title("Lookup: $ip");
            $output_popup->set_default_size(420, 500);
            $output_popup->set_position('center-on-parent');
            $output_popup->set_transient_for($popup);
            $output_popup->set_destroy_with_parent(1);
            
            my $output_sw = Gtk2::ScrolledWindow->new(undef, undef);
            $output_sw->set_policy('automatic', 'automatic');
            
            my $output_tv = Gtk2::TextView->new();
            $output_tv->get_buffer->set_text($output || "No output from script for $ip.");
            
            $output_sw->add($output_tv);
            $output_popup->add($output_sw);
            $output_popup->show_all;
        });
    }

    my $whois_button = Gtk2::Button->new("net whois");
    $lookup_hbox->pack_start($whois_button, 0, 0, 5);
    $whois_button->signal_connect(clicked => sub {
        my $ip = $ip_entry->get_text();
        $ip =~ s/^\s+|\s+$//g;
        unless ($ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
             warn "Invalid IP address format for whois lookup: $ip\n";
             return;
        }
        
        my $output = qx(whois "$ip");
        $output ||= "No output from whois for $ip. Is 'whois' installed?";
        
        my $output_popup = Gtk2::Window->new('toplevel');
        $output_popup->set_title("whois: $ip");
        
        my @lines = split /\n/, $output;
        my $longest_line = @lines ? (sort { length($b) <=> length($a) } @lines)[0] : $output;
        
        my $temp_tv_for_calc = Gtk2::TextView->new();
        my $layout = $temp_tv_for_calc->create_pango_layout($longest_line);
        my ($width_pixels, undef) = $layout->get_pixel_size();
        my $new_width = $width_pixels + 50;
        my $max_width = $state->{map_width} - 20;
        $new_width = $max_width if $new_width > $max_width;
        
        $output_popup->set_default_size($new_width, 400);
        $output_popup->set_position('center-on-parent');
        $output_popup->set_transient_for($popup);
        $output_popup->set_destroy_with_parent(1);
        
        my $output_sw = Gtk2::ScrolledWindow->new(undef, undef);
        $output_sw->set_policy('automatic', 'automatic');
        
        my $output_tv = Gtk2::TextView->new();
        $output_tv->get_buffer->set_text($output);
        
        $output_sw->add($output_tv);
        $output_popup->add($output_sw);
        $output_popup->show_all;
    });

    my $ban_button = Gtk2::Button->new("ban");
    $lookup_hbox->pack_start($ban_button, 0, 0, 5);
    $ban_button->signal_connect(clicked => sub {
        my $ip = $ip_entry->get_text();
        $ip =~ s/^\s+|\s+$//g; # Trim whitespace
        unless ($ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
             warn "Invalid IP address format for ban command: $ip\n";
             set_status_message("Invalid IP for ban: $ip");
             return;
        }

        # Modify the IP to a /24 subnet
        my @octets = split /\./, $ip;
        return unless @octets == 4;
        $octets[3] = 0;
        my $subnet = join('.', @octets) . "/24";

        # Construct the command
        my $command = "sudo iptables -I INPUT -s $subnet -j DROP";

        # Create and show a confirmation dialog
        my $dialog = Gtk2::MessageDialog->new(
            $popup,
            ['modal', 'destroy-with-parent'],
            'question',
            'ok-cancel',
            "Are you sure you want to run this command?\n\n" . $command
        );
        $dialog->set_title("Confirm Ban");
        my $response = $dialog->run;
        $dialog->destroy;

        if ($response eq 'ok') {
            print "Executing: $command\n";
            my $output = qx($command 2>&1); # Capture stdout and stderr

            # Set a status message based on the outcome
            if ($? == 0) {
                set_status_message("Successfully banned subnet: $subnet");
            } else {
                warn "Error executing ban command for $subnet: $output";
                set_status_message("Error banning $subnet. See console.");
            }
        } else {
            set_status_message("Ban command cancelled by user.");
        }
    });

    my $sql_button = Gtk2::Button->new("sql");
    $lookup_hbox->pack_start($sql_button, 0, 0, 5);
    $sql_button->signal_connect(clicked => sub {
        create_sql_query_popup($sql_query_text);
    });

    if (-e $weblog_file_path) {
        my $weblogs_button = Gtk2::Button->new("Weblogs");
        $lookup_hbox->pack_start($weblogs_button, 0, 0, 5);
        $weblogs_button->signal_connect(clicked => sub {
            my $ip = $ip_entry->get_text();
            $ip =~ s/^\s+|\s+$//g;
            my @log_entries = get_ip_logs($ip, $weblog_file_path);
            my $result_text = @log_entries ? join("", @log_entries) : "No log entries found for $ip.";

            my $output_popup = Gtk2::Window->new('toplevel');
            $output_popup->set_title("Weblogs: $ip");

            my $temp_tv_for_calc = Gtk2::TextView->new();
            my $longest_line = @log_entries ? (sort { length($b) <=> length($a) } @log_entries)[0] : $result_text;
            my $layout = $temp_tv_for_calc->create_pango_layout($longest_line);
            my ($width_pixels, undef) = $layout->get_pixel_size();
            my $new_width = $width_pixels + 50;
            my $max_width = $state->{map_width} - 20;
            $new_width = $max_width if $new_width > $max_width;
            
            $output_popup->set_default_size($new_width, 400);
            $output_popup->set_position('center-on-parent');
            $output_popup->set_transient_for($popup);
            $output_popup->set_destroy_with_parent(1);
            
            my $output_sw = Gtk2::ScrolledWindow->new(undef, undef);
            $output_sw->set_policy('automatic', 'automatic');
            
            my $output_tv = Gtk2::TextView->new();
            $output_tv->get_buffer->set_text($result_text);
            
            $output_sw->add($output_tv);
            $output_popup->add($output_sw);
            $output_popup->show_all;
        });
    }

    $main_vbox->pack_start($lookup_hbox, 0, 0, 5);

    my $scrolled_window = Gtk2::ScrolledWindow->new(undef, undef);
    $scrolled_window->set_policy('automatic', 'automatic');
    $scrolled_window->set_shadow_type('in');
    
    my $textview = Gtk2::TextView->new();
    $textview->set_editable(0);
    $textview->set_cursor_visible(0);
    $textview->set_wrap_mode('word');
    
    $textview->modify_base('normal', Gtk2::Gdk::Color->new(0, 0, 0));
    
    my $buffer = $textview->get_buffer();

    for my $point (@$points_ref) {
        my $color = $state->{default_dot_color};
        for my $rule (@{$state->{port_colors}}) {
            my $match = 0;
            if ($rule->{type} eq 'app' && $point->{programname} eq $rule->{target}) {
                $match = 1;
            } elsif ($rule->{type} eq 'local' || $rule->{type} eq 'peer') {
                my $port_to_check = ($rule->{type} eq 'local') ? $point->{local_port} : $point->{port};
                if ($rule->{target} =~ /^(\d+)-(\d+)$/) {
                    if ($port_to_check >= $1 && $port_to_check <= $2) {
                        $match = 1;
                    }
                } elsif ($port_to_check == $rule->{target}) {
                    $match = 1;
                }
            }

            if ($match) {
                $color = $rule->{color};
                last;
            }
        }
        
        my $tag = $buffer->create_tag(undef, 'foreground-gdk' => Gtk2::Gdk::Color->new($color->[0]*65535, $color->[1]*65535, $color->[2]*65535));
        my $iter = $buffer->get_end_iter;
        my $full_label = $point->{full_lookup} // get_point_label_text($point);
        
        my $gps_line = " GPS: $point->{lat}, $point->{lon}";
        if (exists $point->{timestamp}) {
            my $ts_str = strftime("%Y-%m-%d %H:%M", localtime($point->{timestamp}));
            $gps_line .= " - $ts_str";
        }
        $gps_line .= "\n";

        $buffer->insert_with_tags($iter, $full_label . $gps_line, $tag);
    }
    
    $scrolled_window->add($textview);
    $main_vbox->pack_start($scrolled_window, 1, 1, 0);
    
    $popup->show_all;
}

sub create_db_details_popup {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Database Schema Details");
    $popup->set_default_size(500, 400);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(5);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $sw->set_shadow_type('in');

    my $tv = Gtk2::TextView->new();
    $tv->set_editable(0);
    $tv->set_wrap_mode('word');
    $tv->set_left_margin(5);
    $tv->set_right_margin(5);
    
    my $buffer = $tv->get_buffer();
    my $details_text = <<"DETAILS";
Database: $db_file
Table: detailed_history

This table stores all unique connections. You can use the column names below in your SQL WHERE clauses.

TABLE SCHEMA:
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  timestamp INTEGER NOT NULL,
  ip TEXT NOT NULL,
  port INTEGER NOT NULL,
  local_ip TEXT NOT NULL,
  local_port INTEGER NOT NULL,
  programname TEXT,
  full_lookup TEXT,
  asn INTEGER,
  orgname TEXT,
  lat REAL NOT NULL,
  lon REAL NOT NULL,
  x REAL,
  y REAL,
  count INTEGER

INDEXES:
The following columns are indexed for faster lookups:
- timestamp
- x
- y
DETAILS

    $buffer->set_text($details_text);

    $sw->add($tv);
    $popup->add($sw);
    $popup->show_all;
}

sub create_sql_query_popup {
    my ($prefill_sql) = @_;
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Raw SQLite Query Interface");
    $popup->set_default_size(700, 500);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $vbox = Gtk2::VBox->new(0, 6);
    $popup->add($vbox);

    # --- Input Area ---
    my $input_frame = Gtk2::Frame->new("SQL Query");
    my $input_vbox = Gtk2::VBox->new(0, 5);
    $input_vbox->set_border_width(5);
    
    my $input_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $input_sw->set_policy('automatic', 'automatic');
    $input_sw->set_shadow_type('in');
    
    my $input_tv = Gtk2::TextView->new();
    $input_tv->set_wrap_mode('word');
    $input_sw->add($input_tv);
    
    if (defined $prefill_sql && $prefill_sql ne "") {
        $input_tv->get_buffer->set_text($prefill_sql);
    }
    
    $input_vbox->pack_start($input_sw, 1, 1, 0); 
    
    # --- Examples Area ---
    my $examples_frame = Gtk2::Frame->new("Query Scratchpad / Examples");
    my $examples_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $examples_sw->set_policy('never', 'automatic');
    $examples_sw->set_shadow_type('in');
    $examples_frame->set_border_width(5);

    my $examples_tv = Gtk2::TextView->new();
    $examples_tv->set_editable(1);
    $examples_tv->set_wrap_mode('word');
    my $examples_buffer = $examples_tv->get_buffer();
    my $examples_text = "SELECT * FROM detailed_history WHERE port = 1337;\n"
                      . "SELECT * FROM detailed_history WHERE programname = 'imap-login' ORDER BY timestamp DESC;\n"
                      . "SELECT * FROM detailed_history WHERE port NOT IN (80, 443, 22, 993, 587) ORDER BY timestamp DESC LIMIT 50;\n"
                      . "SELECT * FROM detailed_history WHERE orgname LIKE '%Cloudflare%' AND timestamp > (strftime('%s', 'now') - 3600) ORDER BY timestamp DESC;";
    $examples_buffer->set_text($examples_text);
    $examples_sw->add($examples_tv);
    $input_vbox->pack_start($examples_sw, 0, 0, 0); 
    
    $input_frame->add($input_vbox);
    
    # --- Results Area ---
    my $results_frame = Gtk2::Frame->new("Results");
    my $results_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $results_sw->set_policy('automatic', 'automatic');
    $results_sw->set_shadow_type('in');
    $results_sw->set_border_width(5);

    my $results_tv = Gtk2::TextView->new();
    $results_tv->set_editable(0);
    $results_tv->set_cursor_visible(0);
    $results_tv->set_wrap_mode('word');
    $results_tv->modify_base('normal', Gtk2::Gdk::Color->new(0, 0, 0)); 
    $results_sw->add($results_tv);
    $results_frame->add($results_sw);
    
    # --- Buttons Area ---
    my $button_hbox = Gtk2::HBox->new(0, 10);
    my $run_button = Gtk2::Button->new("Run Query");
    my $db_details_button = Gtk2::Button->new("DB Details");
    
    $button_hbox->pack_start($run_button, 1, 1, 0);
    $button_hbox->pack_start($db_details_button, 0, 0, 0);

    $vbox->pack_start($input_frame, 0, 0, 0); 
    $vbox->pack_start($button_hbox, 0, 0, 0);
    $vbox->pack_start($results_frame, 1, 1, 0); 
    
    # --- Button Logic ---
    $run_button->signal_connect(clicked => sub {
        my $results_buffer = $results_tv->get_buffer();
        my $input_buffer = $input_tv->get_buffer();
        my $sql = $input_buffer->get_text($input_buffer->get_start_iter, $input_buffer->get_end_iter, 0);
        
        $results_buffer->set_text(""); 
        
        return unless $sql =~ /\S/;

        eval {
            my $sth = $dbh->prepare($sql);
            $sth->execute();
            
            if ($sql !~ /^\s*SELECT/i) {
                my $rows_affected = $sth->rows;
                my $info_tag = $results_buffer->create_tag(undef, 'foreground' => '#00FF00'); # Bright green
                my $iter = $results_buffer->get_end_iter;
                $results_buffer->insert_with_tags(
                    $iter,
                    "Query executed successfully. Rows affected: $rows_affected\n",
                    $info_tag
                );
                $sth->finish();
                return;
            }

            my $results = $sth->fetchall_hashref('id');
            $sth->finish();

            unless (keys %$results) {
                 my $info_tag = $results_buffer->create_tag(undef, 'foreground' => '#00FF00'); # Bright green
                 my $iter = $results_buffer->get_end_iter;
                 $results_buffer->insert_with_tags(
                     $iter,
                     "Query executed successfully. No rows returned.\n",
                     $info_tag
                 );
                 return;
            }

            for my $id (sort { $a <=> $b } keys %$results) {
                my $point = $results->{$id};
                
                my $color = $state->{default_dot_color};
                for my $rule (@{$state->{port_colors}}) {
                    my $match = 0;
                    if ($rule->{type} eq 'app' && ($point->{programname} // '') eq $rule->{target}) {
                        $match = 1;
                    } elsif ($rule->{type} eq 'local' || $rule->{type} eq 'peer') {
                        my $port_to_check = ($rule->{type} eq 'local') ? $point->{local_port} : $point->{port};
                        if ($rule->{target} =~ /^(\d+)-(\d+)$/) {
                            if ($port_to_check >= $1 && $port_to_check <= $2) { $match = 1; }
                        } elsif ($port_to_check == $rule->{target}) {
                            $match = 1;
                        }
                    }
                    if ($match) { $color = $rule->{color}; last; }
                }
                
                my $tag = $results_buffer->create_tag(undef, 
                    'foreground-gdk' => Gtk2::Gdk::Color->new($color->[0]*65535, $color->[1]*65535, $color->[2]*65535)
                );
                
                my $iter = $results_buffer->get_end_iter;
                my $full_label = $point->{full_lookup} // get_point_label_text($point);
                my $ts_str = strftime("%Y-%m-%d %H:%M", localtime($point->{timestamp}));
                my $line = "$full_label GPS: $point->{lat}, $point->{lon} - $ts_str\n";
                
                $results_buffer->insert_with_tags($iter, $line, $tag);
            }
        };
        if ($@) {
            my $error_tag = $results_buffer->create_tag(undef, 'foreground' => 'red');
            my $iter = $results_buffer->get_end_iter;
            $results_buffer->insert_with_tags($iter, "SQL Error:\n$@", $error_tag);
        }
    });

    $db_details_button->signal_connect(clicked => sub { create_db_details_popup(); });

    $popup->show_all;
}

sub handle_right_click {
    my ($click_x, $click_y) = @_;
    my @nearby_points;
    my %seen_connections;
    my $radius = 15;
    my $radius_squared = $radius * $radius;

    my $x_min = $click_x - $radius;
    my $x_max = $click_x + $radius;
    my $y_min = $click_y - $radius;
    my $y_max = $click_y + $radius;
    my $right_click_sql_query = "SELECT * FROM detailed_history WHERE x BETWEEN $x_min AND $x_max AND y BETWEEN $y_min AND $y_max ORDER BY timestamp DESC;";

    # First, check current points from in-memory state
    for my $point (@{$state->{current_points}}) {
        my ($px, $py) = latlon_to_xy($point->{lat}, $point->{lon});
        my $dist_squared = ($px - $click_x)**2 + ($py - $click_y)**2;
        if ($dist_squared <= $radius_squared) {
            my $key = "$point->{local_ip}:$point->{local_port}:$point->{ip}:$point->{port}";
            unless ($seen_connections{$key}) {
                push @nearby_points, $point;
                $seen_connections{$key} = 1;
            }
        }
    }

    # Always check the detailed history to populate the list fully.
    eval {
        my $sql = "SELECT * FROM detailed_history WHERE x BETWEEN ? AND ? AND y BETWEEN ? AND ? ORDER BY timestamp DESC";
        my $sth = $dbh->prepare($sql);
        
        $sth->execute($x_min, $x_max, $y_min, $y_max);
        
        while (my $point = $sth->fetchrow_hashref('NAME_lc')) {
             my $dist_squared = ($point->{x} - $click_x)**2 + ($point->{y} - $click_y)**2;
             if ($dist_squared <= $radius_squared) {
                my $key = "$point->{local_ip}:$point->{local_port}:$point->{ip}:$point->{port}";
                 unless ($seen_connections{$key}) {
                    push @nearby_points, $point;
                    $seen_connections{$key} = 1;
                }
             }
        }
        $sth->finish();
    };
    if ($@) {
        warn "Failed to query detailed history DB on right-click: $@";
    }
    
    my $ip_entry;
    create_info_popup(\@nearby_points, \$ip_entry, $right_click_sql_query) if @nearby_points;
    
    if (@nearby_points && $ip_entry) {
        $ip_entry->set_text($nearby_points[0]{ip});
    }
    
    $state->{right_click_circle} = { x => $click_x, y => $click_y, radius => $radius };
    $drawing_area->queue_draw();

    Glib::Timeout->add(500, sub {
        $state->{right_click_circle} = undef;
        $drawing_area->queue_draw();
        return 0;
    });
}

sub handle_hilbert_right_click {
    my ($click_x, $click_y, $widget) = @_;
    my @nearby_points;
    my $radius = 15;
    my $radius_squared = $radius * $radius;

    my ($win_width, $win_height) = $widget->window->get_size;
    my $margin = 5;
    my $key_width = 80;
    my $padding = 10;
    my $curve_area_size = $win_height - (2 * $margin);
    my $curve_x_offset = $key_width + $padding + $margin;
    my $curve_y_offset = $margin;
    my $hilbert_grid_size = 2**16;

    for my $point (@{$state->{current_points}}) {
        my $ip_decimal = ip_to_decimal($point->{ip});
        my ($hx, $hy) = ip_decimal_to_hilbert_xy($ip_decimal);

        my $scaled_x = ($hx / ($hilbert_grid_size - 1)) * $curve_area_size + $curve_x_offset;
        my $scaled_y = ($hy / ($hilbert_grid_size - 1)) * $curve_area_size + $curve_y_offset;

        my $dist_squared = ($scaled_x - $click_x)**2 + ($scaled_y - $click_y)**2;
        if ($dist_squared <= $radius_squared) {
            push @nearby_points, $point;
        }
    }
    
    my $ip_entry;
    create_info_popup(\@nearby_points, \$ip_entry) if @nearby_points;
    
    if (@nearby_points && $ip_entry) {
        $ip_entry->set_text($nearby_points[0]{ip});
    }

    $state->{hilbert_right_click_circle} = { x => $click_x, y => $click_y, radius => $radius };
    $state->{hilbert_area}->queue_draw();

    Glib::Timeout->add(500, sub {
        $state->{hilbert_right_click_circle} = undef;
        $state->{hilbert_area}->queue_draw() if $state->{hilbert_area};
        return 0;
    });
}

sub create_pruning_popup {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("History Pruning Settings");
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $vbox = Gtk2::VBox->new(0, 10);
    $popup->add($vbox);

    my $detail_checkbox = Gtk2::CheckButton->new("Enable detailed (SQLite) history recording");
    $detail_checkbox->set_active($state->{detailed_history_enabled});
    $detail_checkbox->signal_connect(toggled => sub {
        $state->{detailed_history_enabled} = $detail_checkbox->get_active();
        set_status_message("Detailed history recording " . ($state->{detailed_history_enabled} ? "enabled" : "disabled"));
    });
    $vbox->pack_start($detail_checkbox, 0, 0, 0);

    my $checkbox = Gtk2::CheckButton->new("Enable automatic history pruning");
    $checkbox->set_active($pruning_enabled);
    $checkbox->signal_connect(toggled => sub {
        $pruning_enabled = $checkbox->get_active();
        set_status_message("History pruning " . ($pruning_enabled ? "enabled" : "disabled"));
    });
    $vbox->pack_start($checkbox, 0, 0, 0);
    
    my $hbox = Gtk2::HBox->new(0, 5);
    my $label = Gtk2::Label->new("History prune time (hours):");
    my $entry = Gtk2::Entry->new();
    $entry->set_text($historytimelimit);
    my $button = Gtk2::Button->new("Set");

    $hbox->pack_start($label, 0, 0, 0);
    $hbox->pack_start($entry, 1, 1, 0);
    $hbox->pack_start($button, 0, 0, 0);
    $vbox->pack_start($hbox, 0, 0, 0);

    $button->signal_connect(clicked => sub {
        my $text = $entry->get_text();
        if ($text =~ /^\d*\.?\d+$/ && $text > 0) {
            $historytimelimit = $text;
            set_status_message("History time limit set to $text hours");
            $popup->destroy();
        } else {
            set_status_message("Invalid input. Please enter a positive number.");
        }
    });

    $popup->show_all;
}


#======================================================================
# Network Handling Subroutines
#======================================================================

sub set_non_blocking {
    my ($socket) = @_;
    my $flags = fcntl($socket, F_GETFL, 0) or die "Can't get flags for socket: $!";
    fcntl($socket, F_SETFL, $flags | O_NONBLOCK) or die "Can't set non-blocking flag: $!";
}

sub close_client {
    my ($sock, $reason) = @_;
    if (fileno($sock)) {
        my $fileno = fileno($sock);
        my $client_ip = eval { $sock->peerhost() };
        return unless $client_ip;

        set_status_message("Client $client_ip disconnected (fd:$fileno): $reason");
        
        delete $state->{client_connections}->{$fileno};
        delete $client_buffers{$fileno};
        delete $client_auth_state{$fileno};
        delete $state->{client_visibility}->{$client_ip};
    }
    close $sock;
}

sub handle_network_events {
    if (my $client_sock = $main::server_sock->accept) {
        my $client_ip = $client_sock->peerhost();
        set_non_blocking($client_sock);
        push @client_sockets, $client_sock;
        my $fileno = fileno($client_sock);
        
        $client_buffers{$fileno} = { buf => '', len => -1 };
        $client_auth_state{$fileno} = 0;
        $state->{client_visibility}->{$client_ip} = 1;
        
        set_status_message("Accepted new client from $client_ip (fd:$fileno), awaiting auth...");
    }

    my @sockets_to_keep;
    foreach my $sock (@client_sockets) {
        my $fileno = fileno($sock);
        my $socket_is_still_valid = 1;

        my $buf;
        my $bytes_read = sysread($sock, $buf, 16384);

        if (defined $bytes_read) {
            if ($bytes_read > 0) {
                $client_buffers{$fileno}->{buf} .= $buf;
                
                PROCESS_BUFFER:
                while ($socket_is_still_valid) {
                    my $b = $client_buffers{$fileno};
                    
                    if ($b->{len} == -1) {
                        if (length($b->{buf}) >= 4) {
                            $b->{len} = unpack('N', substr($b->{buf}, 0, 4, ''));
                        } else {
                            last PROCESS_BUFFER;
                        }
                    }
                    
                    if ($b->{len} != -1 && length($b->{buf}) >= $b->{len}) {
                        my $payload = substr($b->{buf}, 0, $b->{len}, '');
                        
                        if (!$client_auth_state{$fileno}) {
                            my $received_pass = eval { ${thaw($payload)} };
                            if ($@ || !defined $received_pass) {
                                close_client($sock, "Malformed auth packet");
                                $socket_is_still_valid = 0;
                                last PROCESS_BUFFER;
                            }

                            if ($received_pass eq $required_password) {
                                $client_auth_state{$fileno} = 1;
                                set_status_message("Client " . $sock->peerhost() . " (fd:$fileno) authenticated");
                            } else {
                                close_client($sock, "Authentication failed");
                                $socket_is_still_valid = 0;
                                last PROCESS_BUFFER;
                            }
                        } else {
                            my $connections_ref = eval { thaw($payload) };
                            if ($@) {
                                warn "Failed to deserialize data from client (fd:$fileno): $@\n";
                            } else {
                                $state->{client_connections}->{$fileno} = $connections_ref;
                            }
                        }
                        
                        $b->{len} = -1;
                    } else {
                        last PROCESS_BUFFER;
                    }
                }
            } else {
                close_client($sock, "Connection closed by peer");
                $socket_is_still_valid = 0;
            }
        } elsif ($! != EAGAIN) {
            close_client($sock, "Connection error: $!");
            $socket_is_still_valid = 0;
        }

        if ($socket_is_still_valid) {
            push @sockets_to_keep, $sock;
        }
    }
    
    @client_sockets = @sockets_to_keep;
    return 1;
}

#======================================================================
# Main Execution and Exit Handling
#======================================================================
sub on_exit {
    print "\nSaving simple history before exiting...\n";
    save_history_points();
}

$ip2asn_script_path = "ip2asn.pl";
if (-e $ip2asn_script_path && -x $ip2asn_script_path) {
    $ip2asn_lookup_enabled = 1; 
    print "Found executable '$ip2asn_script_path', lookup feature is enabled.\n";
} else { 
    print "Did not find executable 'ip2asn.pl', lookup feature is disabled.\n"; 
}

init_database();

my $pixbuf;
eval { $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file_at_size($image_path, $state->{map_width}, $state->{map_height}); };
die "Error loading image '$image_path': $@" if $@;

$window = Gtk2::Window->new('toplevel');
$window->set_title("connmap-gtk2-server");
$window->set_default_size($state->{map_width}, $state->{map_height});
$window->set_position('center');
$window->set_decorated(0);
$window->set_app_paintable(1);

my $screen = $window->get_screen;
my $rgba_colormap = $screen->get_rgba_colormap;
if ($rgba_colormap) { $window->set_colormap($rgba_colormap); }
else { print "DEBUG: No RGBA colormap available. Transparency may not work.\n"; }

my $fixed_container = Gtk2::Fixed->new;
$drawing_area = Gtk2::DrawingArea->new;
$drawing_area->{_pixbuf_} = $pixbuf;
$drawing_area->set_size_request($state->{map_width}, $state->{map_height});
$fixed_container->put($drawing_area, 0, 0);

if ($show_close_button) {
    my $close_button = Gtk2::Button->new_from_stock('gtk-close');
    $close_button->set_relief('none');
    my $req = $close_button->size_request;
    $fixed_container->put($close_button, $state->{map_width} - $req->width - 5, 5);
    $close_button->signal_connect(clicked => sub { on_exit(); Gtk2->main_quit; });
}

$window->add($fixed_container);
$drawing_area->signal_connect('expose-event' => \&on_draw);
$window->signal_connect(destroy => sub { on_exit(); Gtk2->main_quit; });

load_history_points();
load_port_colors_from_config($config_file);

$window->signal_connect('key-press-event' => sub {
    my ($widget, $event) = @_;
    my $keyname = Gtk2::Gdk->keyval_name($event->keyval);
    if ($keyname eq 'h') {
        $state->{show_history} = !$state->{show_history};
        my $status;
        if ($state->{show_history}) {
            my $count = 0;
            eval {
                my $sth = $dbh->prepare("SELECT COUNT(*) FROM detailed_history");
                $sth->execute();
                ($count) = $sth->fetchrow_array();
                $sth->finish();
            };
            if ($@) {
                warn "Could not get history count: $@";
                $status = "History view: ON";
            } else {
                $status = "History view: ON ($count entries)";
            }
        } else {
            $status = "History view: OFF";
        }

        if (!$state->{detailed_history_enabled}) {
            $status .= " (Detailed history recording is OFF)";
        }
        set_status_message($status);
    } elsif ($keyname eq 't') { 
        $state->{show_text_globally} = !$state->{show_text_globally}; 
        set_status_message("Global text labels: " . ($state->{show_text_globally} ? "ON" : "OFF")); 
    } elsif ($keyname eq 's') { 
        save_history_points(); 
    } elsif ($keyname eq 'l') { 
        load_history_points(); 
    } elsif ($keyname eq 'c') {
        load_port_colors_from_config($config_file);
        set_status_message("Reloaded config from $config_file");
    } elsif ($keyname eq 'a') {
        $state->{all_text_toggle_state} = !$state->{all_text_toggle_state};
        for my $rule (@{$state->{port_colors}}) {
            $rule->{show_text} = $state->{all_text_toggle_state};
        }
        set_status_message("All rule texts turned " . ($state->{all_text_toggle_state} ? "ON" : "OFF"));
    } elsif ($keyname eq 'k') {
        $state->{show_client_list} = 1;
        $drawing_area->queue_draw();
    } elsif ($keyname eq 'p') {
        create_pruning_popup();
    } elsif ($keyname eq 'i') {
        my $client_list_window = Gtk2::Window->new('toplevel');
        $client_list_window->set_title("Connected Clients");
        $client_list_window->set_default_size(300, 200);
        $client_list_window->set_position('center-on-parent');
        $client_list_window->set_transient_for($window);
        $client_list_window->set_destroy_with_parent(1);
        my $sw = Gtk2::ScrolledWindow->new(undef, undef);
        $sw->set_policy('automatic', 'automatic');
        my $tv = Gtk2::TextView->new();
        my $buffer = $tv->get_buffer;
        my $text = "";
        for my $sock (@client_sockets) {
            $text .= "FD: " . fileno($sock) . " IP: " . $sock->peerhost() . "\n";
        }
        $buffer->set_text($text);
        $sw->add($tv);
        $client_list_window->add($sw);
        $client_list_window->show_all;
    } elsif ($keyname eq 'r') {
        $state->{history_points} = {};
        eval {
            $dbh->do("DELETE FROM detailed_history");
            $dbh->do("VACUUM");
        };
        set_status_message("Both simple and detailed history have been reset");
    } elsif ($keyname eq 'z') {
        create_hilbert_window();
    } elsif ($keyname eq 'q') {
        create_sql_query_popup();
    } elsif ($keyname eq 'Escape') { 
        set_status_message("Exiting..."); 
        on_exit();
        Gtk2->main_quit; 
    }
});

my $drag_info = {};
$window->add_events(['button-press-mask', 'button1-motion-mask', 'button-release-mask', 'key-press-mask']);

$window->signal_connect('button-press-event' => sub {
    my ($widget, $event) = @_;
    my ($click_x, $click_y) = $event->get_coords;

    if ($event->button == 1) { # Left click
        for my $area (@{$state->{color_key_areas}}) {
            if ($click_x >= $area->{x_box} && $click_x <= ($area->{x_box} + $area->{w_box}) &&
                $click_y >= $area->{y_box} && $click_y <= ($area->{y_box} + $area->{h_box})) {
                
                my $label_clicked = $area->{label};
                if ($label_clicked eq 'Other') {
                    set_status_message("'Other' is not a toggleable rule"); return 1;
                }
                my $new_state;
                for my $rule (@{$state->{port_colors}}) {
                    if ($rule->{label} eq $label_clicked) {
                        $rule->{show_text} = !$rule->{show_text};
                        $new_state = $rule->{show_text};
                    }
                }
                set_status_message(sprintf("Rule '%s' text: %s", $label_clicked, ($new_state ? "ON" : "OFF")));
                $drawing_area->queue_draw(); return 1;
            }
            elsif ($click_x >= $area->{x_text} && $click_x <= ($area->{x_text} + $area->{w_text}) &&
                   $click_y >= $area->{y_text} && $click_y <= ($area->{y_text} + $area->{h_text})) {
                
                my $points = $area->{points_ref}; my $label = $area->{label};
                if ($points && @$points) { create_connection_list_popup($label, $points); } 
                else { set_status_message("No active connections for rule: $label"); }
                return 1;
            }
        }
        
        if ($state->{show_client_list}) {
            if (my $area = $state->{client_list_close_area}) {
                if ($click_x >= $area->{x} && $click_x <= ($area->{x} + $area->{w}) && $click_y >= $area->{y} && $click_y <= ($area->{y} + $area->{h})) {
                    $state->{show_client_list} = 0; $drawing_area->queue_draw(); return 1;
                }
            }
            for my $area (@{$state->{client_list_areas}}) {
                 if ($click_x >= $area->{x} && $click_x <= ($area->{x} + $area->{w}) && $click_y >= $area->{y} && $click_y <= ($area->{y} + $area->{h})) {
                    $state->{client_visibility}->{$area->{ip}} = !$state->{client_visibility}->{$area->{ip}};
                    $drawing_area->queue_draw(); return 1;
                 }
            }
        }

        if ($state->{show_help_text} && (my $area = $state->{help_text_area})) {
            if ($click_x >= $area->{x} && $click_x <= ($area->{x} + $area->{w}) && $click_y >= $area->{y} && $click_y <= ($area->{y} + $area->{h})) {
                $state->{show_help_text} = 0; $drawing_area->queue_draw(); return 1;
            }
        }
        
        my ($root_x, $root_y) = $event->get_root_coords;
        my ($win_x, $win_y) = $widget->get_position;
        $drag_info->{x} = $root_x - $win_x;
        $drag_info->{y} = $root_y - $win_y;
        return 1;
    }
    elsif ($event->button == 3) { # Right click
        handle_right_click($click_x, $click_y);
        return 1;
    }
    return 0;
});

$window->signal_connect('motion-notify-event' => sub {
    my ($widget, $event) = @_;
    if (exists $drag_info->{x}) {
        my ($root_x, $root_y) = $event->get_root_coords;
        $widget->move($root_x - $drag_info->{x}, $root_y - $drag_info->{y});
    }
});
$window->signal_connect('button-release-event' => sub {
    $drag_info = {};
});

our $server_sock = IO::Socket::INET->new(
    LocalPort => $listen_port,
    Type      => SOCK_STREAM,
    Reuse     => 1,
    Listen    => 10,
) or die "Could not create server socket on port $listen_port: $!\n";
set_non_blocking($server_sock);

Glib::Timeout->add(100, \&handle_network_events);
Glib::Timeout->add(1000, \&process_and_draw, $drawing_area);
Glib::Timeout->add(600000, \&prune_history);
Glib::Timeout->add(250, \&process_db_queue); 

print "Server listening on port $listen_port...\n";
$window->show_all;
process_and_draw($drawing_area);
Gtk2->main;

# Final cleanup after main loop exits
$dbh->disconnect();
close($server_sock);
foreach my $sock (@client_sockets) {
    close($sock);
}

exit 0;
