#!/usr/bin/env perl
#
# A script that automates pasting to a number of pastebin services.
#
# This script is a Perl port of the original wgetpaste bash script. It preserves
# all original functionality, command-line flags, and configuration file behavior
# while offering significant improvements in readability, maintainability, and
# robustness by using modern Perl practices.
#
# Copyright (c) 2007-2016 Bo Ã˜rsted Andresen <bo.andresen@zlin.dk>
# Perl port, documentation, and final bug fixes by Jory A. Pratt <geekypenguin@gmail.com>.

use strict;
use warnings;
use utf8;
use open ':std', ':encoding(UTF-8)'; # Ensure UTF-8 for STDIN/STDOUT/STDERR

# Standard Perl modules for core functionality
use Getopt::Long qw(GetOptions); # For robust command-line option parsing
Getopt::Long::Configure('bundling');  # Enable case-sensitive short options
use File::Temp qw(tempfile);   # For creating secure temporary files
use Fcntl qw(:flock);          # For file locking
use IPC::Open3;                # For securely executing external commands without a shell
use Symbol 'gensym';           # For creating anonymous filehandles for IPC::Open3

# Try to load non-core and optional modules with fallbacks
our $HAS_JSON = eval { require JSON::PP; JSON::PP->import(); 1; };
our $HAS_URI_ESCAPE = eval { require URI::Escape; URI::Escape->import('uri_escape_utf8'); 1; };

our $VERSION = "1.1.0-improved";

# Track temp files for cleanup
our @TEMP_FILES;

# Cleanup handler to ensure temp files are removed
END {
    for my $file (@TEMP_FILES) {
        unlink $file if -f $file;
    }
}

#--------------------------------------------------------------------------#
# Service Definitions
#
# This is the central data structure for the entire script. Each key is a
# service name, and its value is a hash reference containing all the
# configuration for that service. This design makes adding or modifying
# a service clean and simple.
#
# Common keys:
#   engine:             The internal name for the pastebin engine.
#   url:                The API endpoint URL to which we POST.
#   post_sub:           A reference to the subroutine that generates the POST body.
#   regex_url:          A regular expression to extract the final paste URL from wget's output.
#   regex_raw:          A regex pattern to identify parts of a URL for raw conversion.
#   regex_raw_replace:  The replacement string for the raw conversion (uses \1, \2 etc.).
#   default_language:   The default language/syntax if not specified.
#   default_expiration: The default expiration if not specified.
#   languages:          An array reference of supported language names.
#   expirations:        An array reference of supported expiration options.
#   language_values:    A hash mapping human-readable language names to API-specific values.
#   additional_headers: A hash of extra HTTP headers to send (e.g., Content-Type).
#
#--------------------------------------------------------------------------#
my %services = (
    '0x0' => {
        engine     => '0x0',
        url        => 'https://0x0.st',
        post_sub   => \&post_multipart_file,
        regex_url  => qr/^(https?:.+)/,
        size_limit => 512 * 1024 * 1024, # 512MB
    },
    'bpaste' => {
        engine              => 'pinnwand',
        url                 => 'https://bpa.st/api/v1/paste',
        default_language    => 'text',
        default_expiration  => '1week',
        languages           => [qw(text bash c cpp csharp css html java javascript json make perl php python rust)],
        expirations         => [qw(1day 1week)],
        post_sub            => \&post_pinnwand,
        regex_url           => qr/"link":\s*"([^"]+bpa\.st[^"]+)"/,
        regex_raw           => qr|^(https://bpa.st)(/.*)|,
        regex_raw_replace   => '\1/raw\2',
        additional_headers  => { 'Content-Type' => 'application/json' },
        size_limit          => 1 * 1024 * 1024, # 1MB
    },
    'codepad' => {
        engine     => 'codepad',
        url        => 'http://codepad.org/',
        languages  => [ 'C', 'C++', 'D', 'Haskell', 'Lua', 'OCaml', 'PHP', 'Perl', 'Plain Text', 'Python', 'Ruby', 'Scheme', 'Tcl' ],
        post_sub   => \&post_codepad,
        regex_url  => qr/--(http:\/\/codepad\.org\/[^ ]+)/,
        regex_raw  => qr|^(http://[^/]+/)([\w\d]+)$|,
        regex_raw_replace => '\1\2/raw.rb',
    },
    'dpaste' => {
        engine              => 'dpaste',
        url                 => 'http://dpaste.com/api/v2/',
        default_expiration  => '1',
        languages           => [ 'Plain Text', 'Apache Config', 'Bash', 'CSS', 'Diff', 'Django Template/HTML', 'Haskell', 'JavaScript', 'Python', 'Python Interactive/Traceback', 'Ruby', 'Ruby HTML (ERB)', 'SQL', 'XML' ],
        language_values     => { 'Plain Text' => '', 'Apache Config' => 'Apache', 'Bash' => 'Bash', 'CSS' => 'Css', 'Diff' => 'Diff', 'Django Template/HTML' => 'DjangoTemplate', 'Haskell' => 'Haskell', 'JavaScript' => 'JScript', 'Python' => 'Python', 'Python Interactive/Traceback' => 'PythonConsole', 'Ruby' => 'Ruby', 'Ruby HTML (ERB)' => 'Rhtml', 'SQL' => 'Sql', 'XML' => 'Xml' },
        expirations         => [ 1..365 ],
        post_sub            => \&post_dpaste,
        regex_url           => qr/^(http.+)/,
        regex_raw           => qr/^(http.+)$/,
        regex_raw_replace   => '\1.txt',
    },
    'gists' => {
        engine              => 'gists',
        url                 => 'https://api.github.com/gists',
        default_language    => 'Auto',
        languages           => [ 'Auto', 'Bash', 'C', 'C++', 'Go', 'HTML', 'Java', 'JavaScript', 'JSON', 'Perl', 'PHP', 'Python', 'Ruby', 'Rust', 'SQL', 'YAML' ],
        language_values     => { 'Auto' => '', 'Bash' => 'sh', 'C' => 'c', 'C++' => 'cpp', 'Go' => 'go', 'HTML' => 'html', 'Java' => 'java', 'JavaScript' => 'js', 'JSON' => 'json', 'Perl' => 'pl', 'PHP' => 'php', 'Python' => 'py', 'Ruby' => 'rb', 'Rust' => 'rs', 'SQL' => 'sql', 'YAML' => 'yml' },
        post_sub            => \&post_gists,
        regex_url           => qr/"html_url":\s*"([^"]+gist[^"]+)"/,
        regex_raw           => qr|^(https://gist.github.com)(/.*)|,
        regex_raw_replace   => '\1\2/raw',
        additional_headers  => { 'Content-Type' => 'application/json' },
    },
    'ix_io' => {
        engine     => 'ix_io',
        url        => 'https://ix.io',
        post_sub   => \&post_ix_io,
        regex_url  => qr/^(https?:.+)/,
        size_limit => 1 * 1024 * 1024, # 1MB
    },
    'pgz' => {
        engine     => 'pgz',
        url        => 'https://paste.gentoo.zip',
        post_sub   => \&post_multipart_file,
        regex_url  => qr/^(http.+)/,
    },
    'snippets' => {
        engine              => 'snippets',
        url                 => 'https://gitlab.com/api/v4/snippets',
        post_sub            => \&post_snippets,
        regex_url           => qr/"web_url":"([^"]*)"/,
        regex_raw           => qr|^(.*)/snippets(/.*)$|,
        regex_raw_replace   => '\1\2/raw',
        additional_headers  => { 'Content-Type' => 'application/json' },
    },
    'sprunge' => {
        engine     => 'sprunge',
        url        => 'http://sprunge.us',
        post_sub   => \&post_sprunge,
        regex_url  => qr/^(http.+)/,
    },
    'tinyurl' => {
        engine    => 'tinyurl',
        url       => 'http://tinyurl.com/api-create.php',
        post_sub  => \&post_tinyurl,
        regex_url => qr/^(http.+)/,
    },
);

#--------------------------------------------------------------------------#
# POST Body Generation Subroutines
#
# Each of these subroutines is responsible for creating the exact
# string that will be sent as the body of the POST request for a specific
# service or type of service.
#--------------------------------------------------------------------------#

# URL encoding fallback if URI::Escape is not available
sub uri_escape_fallback {
    my ($str) = @_;
    $str =~ s/([^A-Za-z0-9\-_.~])/sprintf("%%%02X", ord($1))/eg;
    return $str;
}

# Wrapper for URL encoding that uses URI::Escape if available, fallback otherwise
sub url_encode {
    my ($str) = @_;
    return $HAS_URI_ESCAPE ? uri_escape_utf8($str) : uri_escape_fallback($str);
}

# Escapes a string for inclusion in a JSON structure.
# Uses JSON::PP if available, otherwise manual escaping
sub json_escape {
    my ($str) = @_;
    if ($HAS_JSON) {
        # Use proper JSON encoding
        my $json = JSON::PP->new->utf8->allow_nonref;
        my $encoded = $json->encode($str);
        # Remove surrounding quotes that encode() adds
        $encoded =~ s/^"|"$//g;
        return $encoded;
    }
    # Fallback to manual escaping
    $str =~ s/\\/\\\\/g; $str =~ s/"/\\"/g; $str =~ s/\t/\\t/g;
    $str =~ s/\r//g;    $str =~ s/\n/\\n/g;
    return $str;
}
# Generates a random boundary string for multipart form data.
sub random_boundary { "WGETPASTE-" . unpack("H*", rand(1e6) . time()) }
# Creates a standard multipart/form-data body for services expecting a file upload.
sub post_multipart_file {
    my ($service, $opts, $content) = @_;
    my $boundary = random_boundary();
    my $filename = $opts->{description};
    my $body = "--$boundary\r\n" .
               "Content-Disposition: form-data; name=\"file\"; filename=\"$filename\"\r\n\r\n" .
               "$content\r\n" .
               "--$boundary--\r\n";
    $service->{additional_headers}{'Content-Type'} = "multipart/form-data; boundary=$boundary";
    return $body;
}
# Creates the specific multipart body for ix.io.
sub post_ix_io {
    my ($service, $opts, $content) = @_;
    my $boundary = random_boundary();
    my $body = "--$boundary\r\n" .
               "Content-Disposition: form-data; name=\"f:1\"\r\n\r\n" .
               "$content\r\n" .
               "--$boundary--\r\n";
    $service->{additional_headers}{'Content-Type'} = "multipart/form-data; boundary=$boundary";
    return $body;
}
# Creates the specific multipart body for sprunge.
sub post_sprunge {
    my ($service, $opts, $content) = @_;
    my $boundary = random_boundary();
    my $body = "--$boundary\r\n" .
               "Content-Disposition: form-data; name=\"sprunge\"\r\n\r\n" .
               "$content\r\n" .
               "--$boundary--\r\n";
    $service->{additional_headers}{'Content-Type'} = "multipart/form-data; boundary=$boundary";
    return $body;
}
# Creates a URL-encoded form body for codepad.
sub post_codepad {
    my ($service, $opts, $content) = @_;
    my %p = ( submit => 'Submit', lang => $opts->{language}, code => url_encode($content) );
    return join('&', map { "$_=$p{$_}" } keys %p);
}
# Creates a JSON body for bpaste (pinnwand engine).
sub post_pinnwand {
    my ($service, $opts, $content) = @_;
    my $desc_esc = json_escape($opts->{description});
    my $cont_esc = json_escape($content);
    return sprintf('{"expiry": "%s", "files": [ {"name": "%s", "lexer": "%s", "content": "%s"} ] }',
        $opts->{expiration}, $desc_esc, $opts->{language}, $cont_esc);
}
# Creates a JSON body for GitHub Gists.
sub post_gists {
    my ($service, $opts, $content) = @_;
    # Respect command-line --private/--public flags first, then config, then default to true
    my $public = 'true';
    if ($opts->{private}) {
        $public = 'false';
    } elsif ($opts->{public}) {
        $public = 'true';
    } elsif ($opts->{config}->{PUBLIC_gists}) {
        $public = $opts->{config}->{PUBLIC_gists};
    }
    my $desc_esc = json_escape($opts->{description});
    my $cont_esc = json_escape($content);
    my $lang_ext = $opts->{language} ? ".$opts->{language}" : "";
    my $filename = $opts->{description};
    $filename =~ s/[^A-Za-z0-9._-]/_/g;
    return sprintf('{"description":"%s","public":%s,"files":{"%s":{"content":"%s"}}}',
        $desc_esc, $public, "$filename$lang_ext", $cont_esc);
}
# Creates a JSON body for GitLab Snippets.
sub post_snippets {
    my ($service, $opts, $content) = @_;
    # Respect command-line --private/--public flags first, then config, then default to public
    my $visibility = 'public';
    if ($opts->{private}) {
        $visibility = 'private';
    } elsif ($opts->{public}) {
        $visibility = 'public';
    } elsif ($opts->{config}->{VISIBILITY_snippets}) {
        $visibility = $opts->{config}->{VISIBILITY_snippets};
    }
    my $desc_esc = json_escape($opts->{description});
    my $cont_esc = json_escape($content);
    return sprintf('{"title":"%s","content":"%s","description":"%s","file_name":"%s","visibility":"%s"}',
        $desc_esc, $cont_esc, $desc_esc, $desc_esc, $visibility);
}
# Creates a multipart body for dpaste.com.
sub post_dpaste {
    my ($service, $opts, $content) = @_;
    my $boundary = random_boundary();
    my $body = "";
    my %form_data = ( title => $opts->{description}, syntax => $opts->{language}, expiry_days => $opts->{expiration}, content => $content );
    for my $name (keys %form_data) {
        $body .= "--$boundary\r\n" .
                 "Content-Disposition: form-data; name=\"$name\"\r\n\r\n" .
                 "$form_data{$name}\r\n";
    }
    $body .= "--$boundary--\r\n";
    $service->{additional_headers}{'Content-Type'} = "multipart/form-data; boundary=$boundary";
    return $body;
}
# Creates a URL-encoded body for tinyurl.
sub post_tinyurl {
    my ($service, $opts, $content) = @_;
    return "url=" . url_encode($content);
}

#--------------------------------------------------------------------------#
# Helper Subroutines
#--------------------------------------------------------------------------#

# Prints a formatted error message and exits with a non-zero status.
sub die_msg { die "Error: @_\n"; }

# Checks if a command-line tool is available in the user's PATH.
sub check_dep {
    my ($cmd) = @_;
    my $path = qx(command -v $cmd 2>/dev/null);
    chomp $path; return $path;
}

# Detects available HTTP client (wget or curl)
sub detect_http_client {
    my $wget = check_dep('wget');
    my $curl = check_dep('curl');
    
    if ($wget) {
        return { tool => 'wget', path => $wget };
    } elsif ($curl) {
        return { tool => 'curl', path => $curl };
    } else {
        die_msg "Neither wget nor curl found. Please install one of them.";
    }
}

# Auto-detect language from file extension
my %EXT_TO_LANG = (
    'pl'   => 'Perl',   'pm'  => 'Perl',
    'py'   => 'Python', 'pyw' => 'Python',
    'rb'   => 'Ruby',   'ru'  => 'Ruby',
    'sh'   => 'Bash',   'bash' => 'Bash',
    'js'   => 'JavaScript', 'mjs' => 'JavaScript',
    'c'    => 'C',      'h'   => 'C',
    'cpp'  => 'C++',    'cxx' => 'C++', 'cc' => 'C++', 'hpp' => 'C++',
    'rs'   => 'Rust',
    'go'   => 'Go',
    'java' => 'Java',
    'php'  => 'PHP',
    'css'  => 'CSS',
    'html' => 'HTML',   'htm' => 'HTML',
    'json' => 'JSON',
    'sql'  => 'SQL',
    'yml'  => 'YAML',   'yaml' => 'YAML',
    'xml'  => 'XML',
    'md'   => 'Plain Text',
);

sub detect_language_from_file {
    my ($filename) = @_;
    return undef unless $filename;
    
    if ($filename =~ /\.([^.]+)$/) {
        my $ext = lc($1);
        return $EXT_TO_LANG{$ext} if exists $EXT_TO_LANG{$ext};
    }
    return undef;
}

# Parse HTTP status code from output and provide helpful error messages
sub parse_http_error {
    my ($output, $exit_code) = @_;
    
    # Common HTTP status codes
    if ($output =~ /HTTP\/\d\.\d\s+(\d{3})\s*(.*)/) {
        my ($code, $msg) = ($1, $2);
        
        if ($code == 401) {
            return "Authentication failed (401 Unauthorized). Check your API token in config file.";
        } elsif ($code == 403) {
            return "Access forbidden (403 Forbidden). You may not have permission to use this service.";
        } elsif ($code == 404) {
            return "Service endpoint not found (404). The service may have changed its API.";
        } elsif ($code == 413) {
            return "Paste too large (413 Payload Too Large). Try a smaller file or different service.";
        } elsif ($code == 429) {
            return "Rate limit exceeded (429 Too Many Requests). Wait a few minutes and try again.";
        } elsif ($code == 500) {
            return "Server error (500 Internal Server Error). The service may be experiencing issues.";
        } elsif ($code == 502 || $code == 503) {
            return "Service unavailable ($code). Try again later or use a different service.";
        } elsif ($code >= 400) {
            return "HTTP error $code: $msg";
        }
    }
    
    # Connection errors
    if ($output =~ /failed: Connection refused/i) {
        return "Connection refused. The service may be down.";
    } elsif ($output =~ /failed: Name or service not known/i || $output =~ /Could not resolve host/i) {
        return "DNS lookup failed. Check your internet connection.";
    } elsif ($output =~ /failed: Connection timed out/i) {
        return "Connection timed out. The service may be slow or down.";
    }
    
    return undef; # No specific error detected
}

# Check configuration file permissions
sub check_config_permissions {
    my ($file) = @_;
    return unless -f $file;
    
    my $mode = (stat($file))[2] & 07777;
    if ($mode & 0044) {  # World or group readable
        warn "Warning: $file is readable by others (mode: " . sprintf("%04o", $mode) . 
             "). This may expose API tokens. Consider running: chmod 600 $file\n";
    }
}
# Securely executes an external command with a list of arguments,
# avoiding shell interpretation of spaces and special characters.
# This uses IPC::Open3 to prevent arguments like "--header=Auth: token ..."
# from being incorrectly split by a shell, which was a critical bug.
sub run_command {
    my @cmd = @_;
    my ($out, $err);
    my $err_handle = gensym;
    my $pid = open3(gensym, $out, $err_handle, @cmd);
    waitpid($pid, 0);
    my $exit_code = $? >> 8;
    my $err_output = do { local $/; <$err_handle> };
    my $output = do { local $/; <$out> };
    # For wget, we care about stderr for redirects and error messages, so we combine them.
    return ($output . $err_output, $exit_code);
}
# Robustly loads configuration files from standard system-wide and user-specific locations.
sub load_configs {
    my ($config_ref, $is_debug) = @_;
    my $home = $ENV{HOME};
    my @config_locations = ( '/etc/wgetpaste.conf', glob('/etc/wgetpaste.d/*.conf'), "$home/.wgetpaste.conf", glob("$home/.wgetpaste.d/*.conf") );
    if ($is_debug) { print STDERR "DEBUG: Checking for config files...\n"; }
    foreach my $file (@config_locations) {
        next unless -f $file && -r $file;
        
        # Check permissions for user config files
        check_config_permissions($file) if $file =~ m|^\Q$home\E|;
        
        if ($is_debug) { print STDERR "DEBUG: Reading config file: $file\n"; }
        open my $fh, '<', $file or next;
        while (my $line = <$fh>) {
            chomp $line;
            $line =~ s/^\s*#.*//;
            $line =~ s/^\s+|\s+$//g;
            next if $line eq '';
            # Regex `([A-Za-z0-9_]+)` includes lowercase letters to correctly parse `HEADER_gists`.
            if ($line =~ /^([A-Za-z0-9_]+)=(.+)$/) {
                my ($key, $val) = ($1, $2);
                $val =~ s/^['"]|['"]$//g;
                $config_ref->{$key} = $val;
            }
        }
        close $fh;
    }
}
# Gathers the content to be pasted from the specified source.
sub get_input {
    my ($opts) = @_;
    my $content = "";
    if    ($opts->{source} eq 'files')   { for my $file (@{$opts->{files}}) { open my $fh, '<', $file or die_msg "Cannot read file: $file"; $content .= "\$ cat $file\n" if @{$opts->{files}} > 1; $content .= do { local $/; <$fh> }; $content .= "\n" if @{$opts->{files}} > 1; close $fh; } }
    elsif ($opts->{source} eq 'command') { for my $cmd (@{$opts->{commands}}) { my ($out) = run_command($cmd); $content .= "\$ $cmd\n$out\n"; } }
    elsif ($opts->{source} eq 'info')    { my $info_cmd = $opts->{config}->{INFO_COMMAND} || 'emerge --info'; my ($out) = run_command($info_cmd); $content = "\$ $info_cmd\n$out"; }
    elsif ($opts->{source} eq 'xcut')    { ($content) = run_command("xclip -o"); }
    elsif ($opts->{source} eq 'url')     { $content = $opts->{url_input}; }
    else                                { $content = do { local $/; <STDIN> }; }
    if ($opts->{info}) { my $info_cmd = $opts->{config}->{INFO_COMMAND} || 'emerge --info'; my ($out) = run_command($info_cmd); $content .= "\n\$ $info_cmd\n$out"; }
    if ($opts->{no_ansi}) { open my $fh, '|-', 'ansifilter' or die_msg "Cannot run ansifilter: $!"; print $fh $content; close $fh; ($content) = run_command("ansifilter"); }
    if ($opts->{tee}) { print STDERR "--- Start of pasted content ---\n$content--- End of pasted content ---\n"; }
    die_msg "No input read. Nothing to paste." unless $content;
    return $content;
}

#--------------------------------------------------------------------------#
# Display and Listing Subroutines
#--------------------------------------------------------------------------#

# Displays the main help message.
sub show_usage {
    my ($default_service, $default_nick) = @_;
    print <<EOF;
Usage: $0 [options] [file[s]]

Options:
    -l, --language LANG           set language (auto-detected from file extension if not set)
    -d, --description DESCRIPTION set description
    -n, --nick NICK               set nick (defaults to "$default_nick")
    -s, --service SERVICE         set service (defaults to "$default_service")
    -e, --expiration EXPIRATION   set when it should expire

    -S, --list-services           list supported pastebin services
    -L, --list-languages          list languages for the specified service
    -E, --list-expirations        list expirations for the specified service

    -u, --tinyurl URL             convert input url to tinyurl

    -c, --command COMMAND         paste COMMAND and its output
    -i, --info                    append system info
    -I, --info-only               paste system info only (Gentoo Linux Only)
    -x, --xcut                    read input from clipboard (requires xclip)
    -X, --xpaste                  write URL to X primary selection (requires xclip)
    -C, --xclippaste              write URL to X clipboard selection (requires xclip)
    -N, --no-ansi                 strip ANSI codes (requires ansifilter)
    -A, --ansi                    don't strip ANSI codes

        --private                 create private paste (for gists/snippets)
        --public                  create public paste (for gists/snippets)

    -r, --raw                     show url for the raw paste
    -t, --tee                     show what is being pasted
    -q, --quiet                   show the url only
    -v, --verbose                 show detailed error output on failure
        --debug                   be *very* verbose (includes HTTP client info)
    -h, --help                    show this help
    -g, --ignore-configs          ignore configuration files
        --version                 show version information

Improvements in v1.1.0:
    - Auto-detects language from file extensions
    - Falls back to curl if wget is not available
    - Better error messages with HTTP status code explanations
    - File size validation before upload
    - Config file permission checks
    - Improved UTF-8 handling
    - Guaranteed temp file cleanup
EOF
    exit 0;
}
# Displays a generic list of options (e.g., languages).
sub show_list {
    my ($title, $items_ref, $default) = @_;
    print "$title:\n";
    if (!@$items_ref) { print "  (not supported by this service)\n"; return; }
    for my $item (sort @$items_ref) { my $d = ($item eq $default) ? '*' : ' '; print "   $d$item\n"; }
}

#--------------------------------------------------------------------------#
# Main Execution
#--------------------------------------------------------------------------#

# 1. Parse Options and Configs
my %opts = ( nick => $ENV{USER} || $ENV{LOGNAME} || getpwuid($<), source => 'stdin', files => [], commands => [] );
my %config;
my $opt_list_services; my $opt_list_langs; my $opt_list_exps;

# Use Getopt::Long to handle all command-line flags.
GetOptions(
    'help|h'              => sub { show_usage($config{DEFAULT_SERVICE} || 'bpaste', $opts{nick}) },
    'version'             => sub { print "$0, version $VERSION\n"; exit 0; },
    'service|s=s'         => \$opts{service}, 'language|l=s' => \$opts{language}, 'expiration|e=s' => \$opts{expiration}, 'description|d=s' => \$opts{description}, 'nick|n=s' => \$opts{nick},
    'tinyurl|u=s'         => sub { $opts{service} = 'tinyurl'; $opts{source} = 'url'; $opts{url_input} = $_[1]; },
    'command|c=s'         => sub { $opts{source} = 'command'; push @{$opts{commands}}, $_[1]; },
    'info|i'              => \$opts{info}, 'info-only|I' => sub { $opts{source} = 'info'; },
    'xcut|x'              => sub { $opts{source} = 'xcut'; }, 'xpaste|X' => \$opts{xpaste}, 'xclippaste|C' => \$opts{xclippaste},
    'no-ansi|N'           => \$opts{no_ansi}, 'ansi|A' => sub { $opts{no_ansi} = 0; },
    'raw|r'               => \$opts{raw}, 'tee|t' => \$opts{tee}, 'quiet|q' => \$opts{quiet},
    'verbose|v'           => \$opts{verbose}, 'debug' => \$opts{debug}, 'ignore-configs|g' => \$opts{ignore_configs},
    'list-services|S'     => \$opt_list_services, 'list-languages|L' => \$opt_list_langs, 'list-expiration|E' => \$opt_list_exps,
    'private'             => \$opts{private}, 'public' => \$opts{public},
) or show_usage($config{DEFAULT_SERVICE} || 'bpaste', $opts{nick});

# 2. Initial Setup and Dependency Checks
$opts{files} = [@ARGV] if @ARGV;
$opts{source} = 'files' if @{$opts{files}};
$opts{verbose} = 1 if $opts{debug};
if ($opts{xpaste} || $opts{xclippaste} || $opts{source} eq 'xcut') { die_msg "--xcut/xpaste/xclippaste requires 'xclip' to be installed." unless check_dep('xclip'); }
if ($opts{no_ansi}) { die_msg "--no-ansi requires 'ansifilter' to be installed." unless check_dep('ansifilter'); }

load_configs(\%config, $opts{debug}) unless $opts{ignore_configs};
$opts{config} = \%config;
if ($opts{debug}) { print STDERR "DEBUG: Config loaded. HEADER_gists = " . ($config{HEADER_gists} || '<not found>') . "\n"; }

# 3. Set Defaults and Validate Service
my $default_service = $config{DEFAULT_SERVICE} || 'bpaste';
$opts{service} ||= $default_service;
die_msg "Service '$opts{service}' is not supported." unless exists $services{$opts{service}};
my $service = $services{$opts{service}};

# Handle listing options as early exits.
if ($opt_list_services) { print "Supported services:\n"; for my $s (sort keys %services) { my $d = ($s eq $default_service) ? '*' : ' '; printf "   %s%-15s %s\n", $d, $s, $services{$s}{url}; } exit 0; }

# 4. Set Dynamic Defaults and Validate
# Auto-detect language from first file if not specified
if (!$opts{language} && $opts{source} eq 'files' && @{$opts{files}}) {
    my $detected = detect_language_from_file($opts{files}[0]);
    $opts{language} = $detected if $detected;
}
$opts{language}   ||= $config{"DEFAULT_LANGUAGE_$opts{service}"} || $service->{default_language} || 'Plain Text';
$opts{expiration} ||= $config{"DEFAULT_EXPIRATION_$opts{service}"} || $service->{default_expiration} || '';

if ($opt_list_langs) { show_list("Languages for $opts{service}", $service->{languages} || [], $opts{language}); exit 0; }
if ($opt_list_exps) { show_list("Expirations for $opts{service}", $service->{expirations} || [], $opts{expiration}); exit 0; }

if ($service->{languages} && !grep { lc $_ eq lc $opts{language} } @{$service->{languages}}) { die_msg "Language '$opts{language}' not supported by $opts{service}. Use --list-languages to see options."; }
if ($service->{expirations} && $opts{expiration} && !grep { lc $_ eq lc $opts{expiration} } @{$service->{expirations}}) { die_msg "Expiration '$opts{expiration}' not supported by $opts{service}. Use --list-expirations to see options."; }
if ($service->{language_values} && exists $service->{language_values}{$opts{language}}) { $opts{language} = $service->{language_values}{$opts{language}}; }

# 5. Prepare Content and Description
$opts{description} ||= $opts{source} eq 'files' ? join(' ', @{$opts{files}}) : $opts{source};
my $content = get_input(\%opts);

# Check size limits
if ($service->{size_limit}) {
    my $size = length($content);
    if ($size > $service->{size_limit}) {
        my $size_mb = sprintf("%.2f", $size / (1024 * 1024));
        my $limit_mb = sprintf("%.2f", $service->{size_limit} / (1024 * 1024));
        die_msg "Content size ($size_mb MB) exceeds $opts{service} limit ($limit_mb MB). Try a different service.";
    }
}

my $post_body = $service->{post_sub}->($service, \%opts, $content);

# 6. Build and Execute the HTTP Command
my ($fh, $filename) = tempfile("wgetpaste.XXXXXX", DIR => "/tmp", UNLINK => 0);  # We'll handle cleanup ourselves
push @TEMP_FILES, $filename;  # Track for cleanup
print $fh $post_body;
flock($fh, LOCK_UN);
close $fh;

# Detect HTTP client
my $http_client = detect_http_client();

my @http_cmd;
if ($http_client->{tool} eq 'wget') {
    @http_cmd = ('wget', '--tries=5', '--timeout=60', '--post-file', $filename, '-O', '-');
    my $header_key = "HEADER_$opts{service}";
    if (exists $config{$header_key} && $config{$header_key}) { push @http_cmd, "--header=$config{$header_key}"; }
    for my $hdr (keys %{$service->{additional_headers} || {}}) { push @http_cmd, "--header=$hdr: $service->{additional_headers}{$hdr}"; }
    push @http_cmd, $service->{url};
    if ($opts{service} eq 'tinyurl') { push @http_cmd, "?$post_body" }
} else {  # curl
    @http_cmd = ('curl', '--retry', '5', '--max-time', '60', '--data-binary', "\@$filename", '-s', '-S');
    my $header_key = "HEADER_$opts{service}";
    if (exists $config{$header_key} && $config{$header_key}) { push @http_cmd, '-H', $config{$header_key}; }
    for my $hdr (keys %{$service->{additional_headers} || {}}) { push @http_cmd, '-H', "$hdr: $service->{additional_headers}{$hdr}"; }
    my $url = $service->{url};
    $url .= "?$post_body" if $opts{service} eq 'tinyurl';
    push @http_cmd, $url;
}

if ($opts{debug}) { 
    print STDERR "DEBUG: Using $http_client->{tool} for HTTP requests\n";
    print STDERR "DEBUG: Executing command array:\n"; 
    require Data::Dumper; 
    print STDERR Data::Dumper->Dump([\@http_cmd], ['http_cmd']); 
}

my ($output, $exit_code) = run_command(@http_cmd);
unlink $filename unless $opts{debug};

# 7. Parse Response and Show URL
my $url;
if ($output =~ $service->{regex_url}) { $url = $1; }

if (!$url) {
    print STDERR "Paste failed. No URL received.\n";
    
    # Try to parse and show specific error
    my $specific_error = parse_http_error($output, $exit_code);
    if ($specific_error) {
        print STDERR "$specific_error\n";
    }
    
    if ($opts{verbose}) {
        print STDERR "\nFull output:\n$output\n";
    } else {
        print STDERR "Enable --verbose for full output.\n";
    }
    
    die_msg "Paste operation failed.";
}

my $raw_text = "";
if ($opts{raw}) {
    if ($service->{regex_raw} && defined $service->{regex_raw_replace}) {
        (my $raw_url = $url) =~ s/$service->{regex_raw}/$service->{regex_raw_replace}/;
        $url = $raw_url;
        $raw_text = "raw ";
    } else { print STDERR "Warning: Raw URL not supported for $opts{service}.\n"; }
}

if ($opts{quiet}) {
    print "$url\n";
} else {
    print "Your ${raw_text}paste can be seen here: $url\n";
}

if ($opts{xpaste} || $opts{xclippaste}) { 
    my $sel = $opts{xpaste} ? 'primary' : 'clipboard'; 
    open my $xclip_fh, '|-', "xclip -selection $sel -loops 10" or die_msg "Failed to run xclip"; 
    print $xclip_fh $url; 
    close $xclip_fh; 
}
exit 0;