#!/usr/bin/env tclsh
# =============================================================================
# ftps-gui — windowed front-end for the ftpsuite. A Midnight-Commander-style
# two-pane view: LOCAL on the left, REMOTE on the right. Multi-select in either
# pane, then Upload -> or <- Download to move the selection, in one go.
#
# Tcl/Tk (wish) — no Python. All config parsing, remote listing, and transfers
# are delegated to the shared shell library (ftps_common.sh) via `sh -c`, so
# this GUI reuses exactly the same logic as ftpush / ftpull / ftps-cli.
#
# Usage: ftps-gui           (just launch it)
#        ftps-gui --version
# =============================================================================

# ── Version / help (before Tk work, so --version doesn't open a window) ──────
if {[llength $argv] > 0} {
    set a [lindex $argv 0]
    if {$a eq "--version" || $a eq "-v"} { puts "ftps-gui (ftpsuite) 1.0.0"; exit 0 }
    if {$a eq "--help" || $a eq "-h"} {
        puts "ftps-gui — windowed two-pane front-end for the ftpsuite."
        puts "Launch with no arguments. Pick a site + pair, select files in either"
        puts "pane, then Upload -> / <- Download. Shares config with the CLI tools."
        exit 0
    }
}

package require Tk

# ── Locate the shared library ────────────────────────────────────────────────
set LIB ""
set selfdir [file dirname [file normalize [info script]]]
set _envlib [expr {[info exists env(FTPS_LIB)] ? $env(FTPS_LIB) : ""}]
foreach c [list $_envlib [file join $selfdir ftps_common.sh] \
                [file join $env(HOME) .local/lib/ftpsuite/ftps_common.sh] \
                /usr/local/lib/ftpsuite/ftps_common.sh] {
    if {$c ne "" && [file exists $c]} { set LIB $c; break }
}
if {$LIB eq ""} {
    tk_messageBox -type ok -icon error -title "ftps-gui" \
        -message "Cannot find ftps_common.sh (set FTPS_LIB)."
    exit 2
}

# Run a shell snippet with the library sourced; return its stdout (trimmed).
proc sh {snippet} {
    global LIB
    set cmd ". [::sh_q $LIB]; $snippet"
    if {[catch {exec sh -c $cmd} out]} { return $out }
    return $out
}
# minimal single-quote for a path passed into sh -c
proc sh_q {s} { return "'[string map {' '\\''} $s]'" }

# ── Global state ─────────────────────────────────────────────────────────────
set ::sites {}        ;# list of {num name host default}
set ::pairs {}        ;# list of {num local remote} for current site
set ::cur_site_num "" ;# selected site number
set ::cur_pair_num "" ;# selected pair number
set ::local_dir ""    ;# current local pane dir
set ::remote_dir ""   ;# current remote pane dir
set ::status "Ready. Click items to toggle multi-select · double-click a folder to open."

# ── Data loaders (delegate to the shell lib) ─────────────────────────────────
proc load_sites {} {
    set ::sites {}
    foreach line [split [sh "ftps_dump_sites"] "\n"] {
        if {$line eq ""} continue
        set f [split $line "\t"]
        lappend ::sites $f
    }
}
proc load_pairs {sitenum} {
    set ::pairs {}
    foreach line [split [sh "ftps_dump_pairs [sh_q $sitenum]"] "\n"] {
        if {$line eq ""} continue
        lappend ::pairs [split $line "\t"]
    }
}
proc remote_ls {sitenum dir} {
    set out [sh "ftps_remote_ls [sh_q $sitenum] [sh_q $dir]"]
    set res {}
    foreach l [split $out "\n"] { if {$l ne ""} { lappend res $l } }
    return $res
}

# ── Pane refresh ─────────────────────────────────────────────────────────────
proc refresh_local {} {
    .panes.l.lb delete 0 end
    .panes.l.path configure -text $::local_dir
    .panes.l.lb insert end ".."
    if {[catch {glob -nocomplain -directory $::local_dir *} entries]} { set entries {} }
    foreach e [lsort $entries] {
        set n [file tail $e]
        if {[file isdirectory $e]} { .panes.l.lb insert end "$n/" } else { .panes.l.lb insert end $n }
    }
}
proc refresh_remote {} {
    .panes.r.lb delete 0 end
    .panes.r.path configure -text $::remote_dir
    .panes.r.lb insert end ".."
    set ::status "Listing $::remote_dir ..."; update idletasks
    foreach e [remote_ls $::cur_site_num $::remote_dir] { .panes.r.lb insert end $e }
    set ::status "Ready."
}

# ── Navigation ───────────────────────────────────────────────────────────────
proc local_activate {} {
    set sel [.panes.l.lb curselection]
    if {$sel eq ""} return
    set item [.panes.l.lb get [lindex $sel 0]]
    if {$item eq ".."} { set ::local_dir [file dirname $::local_dir]; refresh_local; return }
    if {[string match "*/" $item]} {
        set ::local_dir [file join $::local_dir [string trimright $item /]]; refresh_local
    }
}
proc remote_activate {} {
    set sel [.panes.r.lb curselection]
    if {$sel eq ""} return
    set item [.panes.r.lb get [lindex $sel 0]]
    if {$item eq ".."} {
        set d [file dirname $::remote_dir]; if {$d eq ""} {set d "/"}
        set ::remote_dir $d; refresh_remote; return
    }
    if {[string match "*/" $item]} {
        set ::remote_dir [string trimright [file join $::remote_dir [string trimright $item /]] /]
        if {$::remote_dir eq ""} {set ::remote_dir "/"}
        refresh_remote
    }
}

# ── Transfers ────────────────────────────────────────────────────────────────
# Map a local path -> remote via the lib, or remote -> local, honouring pairs.
proc do_upload {} {
    set idxs [.panes.l.lb curselection]
    if {$idxs eq ""} { set ::status "Select local file(s)/folder(s) first."; return }
    set n 0
    foreach i $idxs {
        set item [.panes.l.lb get $i]
        if {$item eq ".."} continue
        set isdir [string match "*/" $item]
        set name [string trimright $item /]
        set lp [file join $::local_dir $name]
        set rp [string trim [sh "ftps_resolve_remote_for [sh_q $::cur_site_num] [sh_q $lp]"]]
        if {$rp eq ""} { set ::status "No pair matches $lp — skipped."; continue }
        set kind [expr {$isdir ? "dir" : "file"}]
        set ::status "Uploading $name ..."; update idletasks
        sh "ftps_transfer_one [sh_q $::cur_site_num] push [sh_q $lp] [sh_q $rp] $kind"
        incr n
    }
    set ::status "Uploaded $n item(s)."
    refresh_remote
}
proc do_download {} {
    set idxs [.panes.r.lb curselection]
    if {$idxs eq ""} { set ::status "Select remote file(s)/folder(s) first."; return }
    set n 0
    foreach i $idxs {
        set item [.panes.r.lb get $i]
        if {$item eq ".."} continue
        set isdir [string match "*/" $item]
        set name [string trimright $item /]
        set rp [string trimright [file join $::remote_dir $name] /]
        set lp [string trim [sh "ftps_resolve_local_for [sh_q $::cur_site_num] [sh_q $rp]"]]
        if {$lp eq ""} { set ::status "No pair matches $rp — skipped."; continue }
        set kind [expr {$isdir ? "dir" : "file"}]
        set ::status "Downloading $name ..."; update idletasks
        sh "ftps_transfer_one [sh_q $::cur_site_num] pull [sh_q $lp] [sh_q $rp] $kind"
        incr n
    }
    set ::status "Downloaded $n item(s)."
    refresh_local
}

# ── Site / pair selection ────────────────────────────────────────────────────
proc on_site_change {} {
    set label [.top.site get]
    # label is "num: name"
    set ::cur_site_num [lindex [split $label ":"] 0]
    load_pairs $::cur_site_num
    set names {}
    foreach p $::pairs { lappend names "[lindex $p 0]: [lindex $p 1] -> [lindex $p 2]" }
    .top.pair configure -values $names
    if {[llength $names] > 0} { .top.pair current 0; on_pair_change }
}
proc on_pair_change {} {
    set label [.top.pair get]
    set ::cur_pair_num [lindex [split $label ":"] 0]
    foreach p $::pairs {
        if {[lindex $p 0] eq $::cur_pair_num} {
            set ::local_dir  [string trimright [lindex $p 1] /]
            set ::remote_dir [string trimright [lindex $p 2] /]
            if {$::local_dir eq ""}  {set ::local_dir "/"}
            if {$::remote_dir eq ""} {set ::remote_dir "/"}
            refresh_local; refresh_remote
            return
        }
    }
}

# ── Theming (dark / light) ───────────────────────────────────────────────────
set ::theme dark
proc theme_palette {mode} {
    if {$mode eq "light"} {
        return [list bg #eef1f6 panel #ffffff fg #1a2233 dim #5b6675 \
                     sel #2f6fd6 selfg #ffffff line #d3dae6 \
                     accL #1f6fd0 accR #0f9d63 btn #e2e8f2 btnfg #1a2233 field #ffffff]
    }
    return [list bg #171e2b panel #10161f fg #e6edf7 dim #8b98b0 \
                 sel #2f6fd6 selfg #ffffff line #232f42 \
                 accL #6db3ff accR #34d399 btn #223049 btnfg #e6edf7 field #0e141d]
}
proc apply_theme {mode} {
    set ::theme $mode
    array set c [theme_palette $mode]
    ttk::style theme use clam
    ttk::style configure . -background $c(bg) -foreground $c(fg) \
        -fieldbackground $c(field) -bordercolor $c(line) -focuscolor $c(sel) \
        -troughcolor $c(panel) -arrowcolor $c(fg)
    ttk::style configure TLabel  -background $c(bg) -foreground $c(fg)
    ttk::style configure TFrame  -background $c(bg)
    ttk::style configure Hdr.TLabel -background $c(bg) -foreground $c(fg) \
        -font {TkDefaultFont 11 bold} -padding {2 4}
    ttk::style configure TButton -background $c(btn) -foreground $c(btnfg) \
        -padding {10 6} -relief flat -borderwidth 0
    ttk::style map TButton -background [list active $c(sel) pressed $c(sel)] \
        -foreground [list active $c(selfg) pressed $c(selfg)]
    ttk::style configure Accent.TButton -background $c(sel) -foreground $c(selfg)
    ttk::style map Accent.TButton -background [list active $c(accL) pressed $c(accL)]
    ttk::style configure TCombobox -fieldbackground $c(field) -background $c(btn) \
        -foreground $c(fg) -arrowcolor $c(fg) -bordercolor $c(line) -padding {6 4}
    ttk::style map TCombobox -fieldbackground [list readonly $c(field)] \
        -foreground [list readonly $c(fg)] -background [list readonly $c(btn)]
    ttk::style configure Vertical.TScrollbar -background $c(btn) \
        -troughcolor $c(panel) -bordercolor $c(bg) -arrowcolor $c(fg)
    option add *TCombobox*Listbox.background $c(field)
    option add *TCombobox*Listbox.foreground $c(fg)
    option add *TCombobox*Listbox.selectBackground $c(sel)
    option add *TCombobox*Listbox.selectForeground $c(selfg)
    . configure -background $c(bg)
    foreach lb {.panes.l.lb .panes.r.lb} {
        $lb configure -background $c(panel) -foreground $c(fg) \
            -selectbackground $c(sel) -selectforeground $c(selfg) \
            -highlightthickness 0 -borderwidth 0 -relief flat \
            -font {TkFixedFont 10} -activestyle none
    }
    catch { .panes configure -background $c(line) -sashwidth 6 -borderwidth 0 }
    .panes.l.path configure -foreground $c(accL)
    .panes.r.path configure -foreground $c(accR)
    .status configure -background $c(panel) -foreground $c(dim)
    .btns.tt configure -text [expr {$mode eq "dark" ? "☀ Light" : "☾ Dark"}]
}
proc toggle_theme {} { apply_theme [expr {$::theme eq "dark" ? "light" : "dark"}] }

# ── Build the UI ─────────────────────────────────────────────────────────────
# Dark base palette FIRST, so even if a ttk style call is a no-op on this Tk
# build, classic widgets (listboxes, frames, root) still start dark.
catch { tk_setPalette background #171e2b foreground #e6edf7 \
        activeBackground #223049 activeForeground #e6edf7 \
        selectBackground #2f6fd6 selectForeground #ffffff \
        highlightBackground #171e2b troughColor #10161f }
wm title . "ftps-gui — ftpsuite"
wm geometry . 940x600
wm minsize . 720 440

ttk::frame .top -padding {8 8 8 4}
ttk::label .top.sl -text "Site"
ttk::combobox .top.site -state readonly -width 26
ttk::label .top.pl -text "Pair"
ttk::combobox .top.pair -state readonly -width 42
pack .top.sl -side left -padx {2 4}
pack .top.site -side left -padx {0 12}
pack .top.pl -side left -padx {2 4}
pack .top.pair -side left
pack .top -side top -fill x
bind .top.site <<ComboboxSelected>> on_site_change
bind .top.pair <<ComboboxSelected>> on_pair_change

panedwindow .panes -orient horizontal
ttk::frame .panes.l -padding 4
ttk::label .panes.l.hdr -text "▏LOCAL" -anchor w -style Hdr.TLabel
ttk::label .panes.l.path -text "" -anchor w
listbox .panes.l.lb -selectmode multiple -yscrollcommand ".panes.l.sb set"
ttk::scrollbar .panes.l.sb -command ".panes.l.lb yview"
pack .panes.l.hdr -side top -fill x
pack .panes.l.path -side top -fill x -pady {0 4}
pack .panes.l.sb -side right -fill y
pack .panes.l.lb -side left -fill both -expand 1
bind .panes.l.lb <Double-1> local_activate
bind .panes.l.lb <Return> local_activate

ttk::frame .panes.r -padding 4
ttk::label .panes.r.hdr -text "▏REMOTE" -anchor w -style Hdr.TLabel
ttk::label .panes.r.path -text "" -anchor w
listbox .panes.r.lb -selectmode multiple -yscrollcommand ".panes.r.sb set"
ttk::scrollbar .panes.r.sb -command ".panes.r.lb yview"
pack .panes.r.hdr -side top -fill x
pack .panes.r.path -side top -fill x -pady {0 4}
pack .panes.r.sb -side right -fill y
pack .panes.r.lb -side left -fill both -expand 1
bind .panes.r.lb <Double-1> remote_activate
bind .panes.r.lb <Return> remote_activate

.panes add .panes.l
.panes add .panes.r
pack .panes -side top -fill both -expand 1 -padx 8 -pady 4

ttk::frame .btns -padding {8 4 8 8}
ttk::button .btns.up   -text "⬆  Upload"   -style Accent.TButton -command do_upload
ttk::button .btns.down -text "⬇  Download" -style Accent.TButton -command do_download
ttk::button .btns.rl   -text "↻ Local"  -command refresh_local
ttk::button .btns.rr   -text "↻ Remote" -command refresh_remote
ttk::button .btns.tt   -text "☀ Light" -command toggle_theme
ttk::button .btns.q    -text "Quit" -command {exit 0}
pack .btns.up .btns.down .btns.rl .btns.rr -side left -padx 3
pack .btns.q .btns.tt -side right -padx 3
pack .btns -side top -fill x

label .status -textvariable ::status -anchor w -relief flat -bd 0 -padx 10 -pady 4
pack .status -side bottom -fill x

apply_theme dark

# ── Populate ─────────────────────────────────────────────────────────────────
if {[catch {load_sites} err]} {
    tk_messageBox -type ok -icon error -title "ftps-gui" -message "Config error:\n$err"
    exit 1
}
if {[llength $::sites] == 0} {
    tk_messageBox -type ok -icon info -title "ftps-gui" \
        -message "No sites configured.\nAdd one first with:  ftpush --add-site"
    exit 0
}
set sitevals {}
foreach s $::sites {
    set d [expr {[lindex $s 3] eq "1" ? " (default)" : ""}]
    lappend sitevals "[lindex $s 0]: [lindex $s 1]$d"
}
.top.site configure -values $sitevals
.top.site current 0
on_site_change
