package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	encrypt "fckeuspy-go/lib"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"sync/atomic"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/layout"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

// --- Core UI model ---
type uiParts struct {
	outKey, msg, peer, cipherOut, payload, plainOut *widget.Entry
	toastLabel                                      *widget.Label
	cipherQR, pubQR, crtQR, peerQR, payloadQR       *canvas.Image
	showQR, showPeerQR, showPayloadQR               bool
}

func buildEntries() *uiParts {
	p := &uiParts{
		outKey:     widget.NewMultiLineEntry(),
		msg:        widget.NewMultiLineEntry(),
		peer:       widget.NewMultiLineEntry(),
		cipherOut:  widget.NewMultiLineEntry(),
		payload:    widget.NewMultiLineEntry(),
		plainOut:   widget.NewMultiLineEntry(),
		toastLabel: widget.NewLabel(""),
		cipherQR:   canvas.NewImageFromImage(nil),
		pubQR:      canvas.NewImageFromImage(nil),
		crtQR:      canvas.NewImageFromImage(nil),
		peerQR:     canvas.NewImageFromImage(nil),
		payloadQR:  canvas.NewImageFromImage(nil),
		showQR:     true, showPeerQR: true, showPayloadQR: true,
	}
	p.cipherQR.SetMinSize(fyne.NewSize(220, 220))
	p.pubQR.SetMinSize(fyne.NewSize(200, 200))
	p.peerQR.SetMinSize(fyne.NewSize(200, 200))
	p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
	p.pubQR.FillMode = canvas.ImageFillContain
	p.toastLabel.Hide()
	return p
}

func (p *uiParts) showToast(s string) {
	fyne.Do(func() { p.toastLabel.SetText(s); p.toastLabel.Show() })
	time.AfterFunc(1500*time.Millisecond, func() {
		fyne.Do(func() {
			if p.toastLabel.Text == s {
				p.toastLabel.Hide()
			}
		})
	})
}

// Theme
type simpleTheme struct{}

var (
	chatIncomingBubbleColor = color.NRGBA{39, 48, 59, 255}
	chatOutgoingBubbleColor = color.NRGBA{28, 84, 113, 255}
	chatFailedBubbleColor   = color.NRGBA{91, 48, 48, 255}
)

func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
	switch n {
	case theme.ColorNameBackground:
		return color.NRGBA{18, 22, 28, 255}
	case theme.ColorNameInputBackground:
		return color.NRGBA{29, 35, 43, 255}
	case theme.ColorNameButton:
		return color.NRGBA{39, 48, 59, 255}
	case theme.ColorNamePrimary:
		return color.NRGBA{99, 179, 237, 255}
	case theme.ColorNameForeground:
		return color.NRGBA{235, 240, 245, 255}
	case theme.ColorNameDisabled:
		// Make disabled text brighter for readability on dark background
		return color.NRGBA{230, 233, 238, 255}
	default:
		return theme.DefaultTheme().Color(n, v)
	}
}
func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource    { return theme.DefaultTheme().Font(st) }
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) }
func (simpleTheme) Size(n fyne.ThemeSizeName) float32       { return theme.DefaultTheme().Size(n) }

// Facade interface
type ServiceFacade interface {
	Encrypt(msg, peer string) (string, error)
	EncryptChat(msg, peer string) (string, error)
	Decrypt(json string) (string, error)
	DecryptChat(json string) (DecryptedChatMessage, error)
	PublicPEM() string
	PublicCert() string
	ListContacts() ([]Contact, error)
	SaveContact(c Contact) error
	DeleteContact(id string) error
	ListChatMessages() ([]ChatMessage, error)
	AppendChatMessage(direction, text, payload string) (ChatMessage, bool, error)
	AppendScannedChatMessage(direction, text, payload string, createdAt time.Time) (ChatMessage, bool, error)
}

// Clipboard helpers
func copyClip(s string, parts *uiParts) {
	fyne.CurrentApp().Clipboard().SetContent(s)
	parts.showToast("Zkopírováno")
}

func copyImageToClipboard(img image.Image, parts *uiParts) {
	if img == nil {
		return
	}
	buf := &bytes.Buffer{}
	if err := png.Encode(buf, img); err != nil {
		parts.showToast("Chyba PNG")
		return
	}
	choose := func() *exec.Cmd {
		wayland := os.Getenv("WAYLAND_DISPLAY") != ""
		has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
		if wayland {
			if has("wl-copy") {
				return exec.Command("wl-copy", "--type", "image/png")
			}
			if has("xclip") {
				return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
			}
		} else {
			if has("xclip") {
				return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
			}
			if has("wl-copy") {
				return exec.Command("wl-copy", "--type", "image/png")
			}
		}
		return nil
	}()
	if choose == nil {
		parts.showToast("Chybí wl-copy/xclip")
		return
	}
	stdin, _ := choose.StdinPipe()
	if err := choose.Start(); err != nil {
		parts.showToast("Nelze spustit")
		return
	}
	_, _ = stdin.Write(buf.Bytes())
	_ = stdin.Close()
	if err := choose.Wait(); err != nil {
		parts.showToast("Selhalo")
		return
	}
	parts.showToast("QR obrázek ve schránce")
}

func readImageClipboard() (image.Image, error) {
	has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
	tryDecode := func(d []byte) (image.Image, bool) {
		if len(d) == 0 {
			return nil, false
		}
		if img, _, e := image.Decode(bytes.NewReader(d)); e == nil {
			return img, true
		}
		s := strings.TrimSpace(string(d))
		if strings.HasPrefix(s, "data:image") {
			if p := strings.Index(s, ","); p > 0 {
				s = s[p+1:]
			}
		}
		if raw, err := base64.StdEncoding.DecodeString(s); err == nil {
			if img, _, e2 := image.Decode(bytes.NewReader(raw)); e2 == nil {
				return img, true
			}
		}
		return nil, false
	}

	if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
		if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
			pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
			seen := map[string]bool{}
			for _, p := range pref {
				seen[p] = true
			}
			order := append([]string{}, pref...)
			for _, t := range strings.Split(string(types), "\n") {
				t = strings.TrimSpace(t)
				if t == "" || !strings.HasPrefix(t, "image/") {
					continue
				}
				if !seen[t] {
					order = append(order, t)
				}
			}
			for _, t := range order {
				if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
					if img, ok := tryDecode(data); ok {
						return img, nil
					}
				}
			}
		}
		if data, err := exec.Command("wl-paste").Output(); err == nil {
			if img, ok := tryDecode(data); ok {
				return img, nil
			}
		}
	}
	if has("xclip") {
		for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
			if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
				if img, ok := tryDecode(data); ok {
					return img, nil
				}
			}
		}
		if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
			if img, ok := tryDecode(data); ok {
				return img, nil
			}
		}
	}
	return nil, errors.New("nenalezen žádný obrázek ve schránce")
}

// Identity tab
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
	// Toolbar: choose what to encode into the QR to keep density manageable
	mode := "cert" // cert, pub
	options := []string{"Certifikát", "Veřejný klíč"}
	chooser := widget.NewSelect(options, func(string) {})
	chooser.Selected = options[0]
	var update func()
	chooser.OnChanged = func(v string) {
		switch v {
		case "Certifikát":
			mode = "cert"
		case "Veřejný klíč":
			mode = "pub"
		default:
			mode = "cert"
		}
		update()
	}
	deleteBtn := widget.NewButton("Smazat identitu", func() {
		pw := widget.NewPasswordEntry()
		form := widget.NewForm(widget.NewFormItem("Heslo", pw))
		warn := widget.NewLabel("Smazat vše?")
		d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", container.NewVBox(warn, form), func(ok bool) {
			if !ok {
				return
			}
			if _, err := encrypt.OpenEncryptedStore(vaultPath, pw.Text); err != nil {
				dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
				return
			}
			_ = os.Remove(vaultPath)
			fyne.CurrentApp().Quit()
		}, fyne.CurrentApp().Driver().AllWindows()[0])
		d.Resize(fyne.NewSize(420, 200))
		d.Show()
	})
	// Keep button minimal; align right
	deleteRow := container.NewHBox(layout.NewSpacer(), deleteBtn)
	makeQR := func(data string, target *canvas.Image) {
		if data == "" {
			target.Image = nil
			target.Refresh()
			return
		}
		if b, err := GenerateQRPNG(data, 512); err == nil {
			if im, err2 := LoadPNG(b); err2 == nil {
				target.Image = im
				target.Refresh()
			}
		}
	}
	update = func() {
		if parts.showQR {
			var text string
			switch mode {
			case "pub":
				text = strings.TrimSpace(svc.PublicPEM())
			default: // cert
				text = strings.TrimSpace(svc.PublicCert())
			}
			makeQR(text, parts.pubQR)
		} else {
			parts.pubQR.Image = nil
			parts.pubQR.Refresh()
		}
	}
	update()
	saveQR := widget.NewButtonWithIcon("Uložit QR", theme.DocumentSaveIcon(), func() {
		if parts.pubQR.Image == nil {
			return
		}
		win := fyne.CurrentApp().Driver().AllWindows()[0]
		img := parts.pubQR.Image
		fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
			if err != nil || wc == nil {
				return
			}
			defer wc.Close()
			_ = png.Encode(wc, img)
			parts.showToast("QR uložen")
		}, win)
		fd.SetFileName("identity_qr.png")
		fd.Show()
	})
	qrRow := container.NewHBox(
		widget.NewLabel("Obsah QR"),
		chooser,
		layout.NewSpacer(),
		widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) }),
		saveQR,
	)
	box := container.NewVBox(qrRow, container.NewCenter(parts.pubQR))
	return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteRow))
}

// Decrypt tab
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
	parts.plainOut.Disable()
	parts.plainOut.Wrapping = fyne.TextWrapWord
	parts.plainOut.SetMinRowsVisible(12)
	parts.payloadQR.FillMode = canvas.ImageFillContain
	parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))

	decrypt := func(text string) {
		trimmed := strings.TrimSpace(text)
		if trimmed == "" {
			parts.plainOut.SetText("")
			return
		}
		go func(j string) {
			res, err := svc.Decrypt(j)
			if err != nil {
				fyne.Do(func() {
					parts.plainOut.SetText("")
					parts.showToast("Chyba dešifrování")
				})
				return
			}
			fyne.Do(func() { parts.plainOut.SetText(res) })
		}(trimmed)
	}

	setPayload := func(cipher string, img image.Image) {
		parts.payloadQR.Image = img
		parts.payloadQR.Refresh()
		decrypt(cipher)
	}

	decodeFromImage := func(img image.Image) {
		if img == nil {
			parts.showToast("Žádný QR obrázek")
			return
		}
		txt, err := DecodeQR(img)
		if err != nil {
			bounds := img.Bounds()
			inv := image.NewRGBA(bounds)
			for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
				for x := bounds.Min.X; x < bounds.Max.X; x++ {
					r, g, b, a := img.At(x, y).RGBA()
					inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
				}
			}
			if txt2, err2 := DecodeQR(inv); err2 == nil {
				setPayload(txt2, img)
				parts.showToast("Načteno z invert QR")
				return
			}
			parts.showToast("QR nenalezen: " + err.Error())
			return
		}
		setPayload(txt, img)
		parts.showToast("Načteno z QR")
	}

	pasteQRBtn := widget.NewButtonWithIcon("Vložit ze schránky", theme.ContentPasteIcon(), func() {
		img, err := readImageClipboard()
		if err != nil {
			parts.showToast("Chyba schránky: " + err.Error())
			return
		}
		decodeFromImage(img)
	})

	openQRBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() {
		win := fyne.CurrentApp().Driver().AllWindows()[0]
		fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
			if err != nil || rc == nil {
				return
			}
			defer rc.Close()
			data, readErr := io.ReadAll(rc)
			if readErr != nil {
				parts.showToast("Chyba čtení souboru")
				return
			}
			img, _, decErr := image.Decode(bytes.NewReader(data))
			if decErr != nil {
				parts.showToast("Neplatný obrázek: " + decErr.Error())
				return
			}
			decodeFromImage(img)
		}, win)
		fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
		fd.Show()
	})

	clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
		parts.payloadQR.Image = nil
		parts.payloadQR.Refresh()
		parts.plainOut.SetText("")
		parts.showToast("Vymazáno")
	})

	// Align buttons to the right by placing spacer first
	toolbar := container.NewHBox(layout.NewSpacer(), pasteQRBtn, openQRBtn, clearBtn)

	copyDecBtn := widget.NewButtonWithIcon("Kopírovat zprávu", theme.ContentCopyIcon(), func() {
		if strings.TrimSpace(parts.plainOut.Text) != "" {
			copyClip(parts.plainOut.Text, parts)
		}
	})
	return container.NewVBox(
		widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		toolbar,
		container.NewHBox(parts.payloadQR, layout.NewSpacer()),
		container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), copyDecBtn),
		parts.plainOut,
	)
}

// Per-contact encryption popup (QR-only output)
func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
	msgEntry := widget.NewMultiLineEntry()
	msgEntry.SetMinRowsVisible(6)
	msgEntry.Wrapping = fyne.TextWrapWord
	status := widget.NewLabel("Zadej zprávu…")
	qrImg := canvas.NewImageFromImage(nil)
	qrImg.FillMode = canvas.ImageFillContain
	qrImg.SetMinSize(fyne.NewSize(300, 300))
	updateQR := func(text string) {
		if strings.TrimSpace(text) == "" {
			qrImg.Image = nil
			qrImg.Refresh()
			return
		}
		if b, err := GenerateQRPNG(text, 512); err == nil {
			if im, err2 := LoadPNG(b); err2 == nil {
				qrImg.Image = im
				qrImg.Refresh()
			}
		}
	}
	win := fyne.CurrentApp().Driver().AllWindows()[0]
	doEncrypt := func() {
		m := strings.TrimSpace(msgEntry.Text)
		fyne.Do(func() {
			if m == "" {
				status.SetText("Zpráva je prázdná")
				updateQR("")
				return
			}
			status.SetText("Šifruji…")
		})
		if m == "" {
			return
		}
		go func(txt string) {
			res, err := svc.Encrypt(txt, ct.Cert)
			if err != nil {
				fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
				return
			}
			fyne.Do(func() { updateQR(res); status.SetText("Hotovo") })
		}(m)
	}
	var tmr *time.Timer
	msgEntry.OnChanged = func(string) {
		if tmr != nil {
			tmr.Stop()
		}
		tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
	}
	copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
	saveQRBtn := widget.NewButton("Uložit QR", func() {
		if qrImg.Image == nil {
			return
		}
		img := qrImg.Image
		fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
			if err != nil || wc == nil {
				return
			}
			defer wc.Close()
			_ = png.Encode(wc, img)
			status.SetText("QR uložen")
		}, win)
		fd.SetFileName("message_qr.png")
		fd.Show()
	})
	content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyQRBtn, saveQRBtn), qrImg, status)
	title := ct.Name
	if title == "" {
		title = "(bez názvu)"
	}
	if cn := extractCN(ct.Cert); cn != "" && !strings.Contains(title, cn) {
		title = fmt.Sprintf("%s (%s)", title, cn)
	}
	dlg := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", title), "Zavřít", content, win)
	dlg.Resize(fyne.NewSize(640, 520))
	dlg.Show()
}

// Contacts tab with QR-only popup
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
	var all, filtered []Contact
	load := func() { items, _ := svc.ListContacts(); all = items; filtered = items }
	apply := func(q string) {
		if q == "" {
			filtered = all
			return
		}
		low := strings.ToLower(q)
		tmp := make([]Contact, 0, len(all))
		for _, c := range all {
			if strings.Contains(strings.ToLower(c.Name), low) || strings.Contains(strings.ToLower(c.Cert), low) {
				tmp = append(tmp, c)
			}
		}
		filtered = tmp
	}
	makeDefault := func() string {
		base := "Nový kontakt"
		exists := false
		maxN := 1
		for _, c := range all {
			if c.Name == base {
				exists = true
			}
			if strings.HasPrefix(c.Name, base+" ") {
				var n int
				if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil && n >= maxN {
					maxN = n + 1
				}
			}
		}
		if !exists {
			return base
		}
		return fmt.Sprintf("%s %d", base, maxN)
	}
	search := widget.NewEntry()
	var list *widget.List
	openPopup := func(existing *Contact) {
		nameEntry := widget.NewEntry()
		if existing != nil {
			nameEntry.SetText(existing.Name)
		} else {
			nameEntry.SetText(makeDefault())
		}
		var certValue string
		if existing != nil {
			certValue = existing.Cert
		}
		qrImg := canvas.NewImageFromImage(nil)
		qrImg.FillMode = canvas.ImageFillContain
		qrImg.SetMinSize(fyne.NewSize(300, 300))
		updateQR := func() {
			if strings.TrimSpace(certValue) == "" {
				qrImg.Image = nil
				qrImg.Refresh()
				return
			}
			if b, err := GenerateQRPNG(certValue, 512); err == nil {
				if im, err2 := LoadPNG(b); err2 == nil {
					qrImg.Image = im
					qrImg.Refresh()
				}
			}
		}
		updateQR()
		pasteQR := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
			img, err := readImageClipboard()
			if err != nil {
				parts.showToast("Chyba čtení schránky: " + err.Error())
				return
			}
			if img == nil {
				parts.showToast("Žádný QR obrázek")
				return
			}

			txt, decErr := DecodeQR(img)
			if decErr != nil {
				bounds := img.Bounds()
				inv := image.NewRGBA(bounds)
				for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
					for x := bounds.Min.X; x < bounds.Max.X; x++ {
						r, g, b, a := img.At(x, y).RGBA()
						inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
					}
				}
				if txt2, err2 := DecodeQR(inv); err2 == nil {
					certValue = txt2
					updateQR()
					parts.showToast("Načteno z invert QR")
					return
				}
				debugDir := "qr_debug"
				_ = os.MkdirAll(debugDir, 0o755)
				fp := filepath.Join(debugDir, fmt.Sprintf("qr_clip_%d.png", time.Now().UnixNano()))
				if f, e := os.Create(fp); e == nil {
					_ = png.Encode(f, img)
					_ = f.Close()
					parts.showToast("QR nenalezen: " + decErr.Error() + " (" + fp + ")")
				} else {
					parts.showToast("QR nenalezen: " + decErr.Error())
				}
				return
			}
			certValue = txt
			updateQR()
			parts.showToast("Načteno z QR")
		})
		openImg := widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
			win := fyne.CurrentApp().Driver().AllWindows()[0]
			fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
				if err != nil || rc == nil {
					return
				}
				defer rc.Close()
				data, _ := io.ReadAll(rc)
				img, _, e2 := image.Decode(bytes.NewReader(data))
				if e2 != nil {
					parts.showToast("Neplatný obrázek: " + e2.Error())
					return
				}

				txt, e3 := DecodeQR(img)
				if e3 != nil {
					parts.showToast("QR nenalezeno: " + e3.Error())
					return
				}
				certValue = txt
				updateQR()
				parts.showToast("Načteno z QR")
			}, win)
			fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
			fd.Show()
		})
		clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; updateQR() })
		toolbar := widget.NewToolbar(pasteQR, openImg, clearAct)
		win := fyne.CurrentApp().Driver().AllWindows()[0]
		var popup dialog.Dialog
		save := func(useEncrypt bool) {
			name := strings.TrimSpace(nameEntry.Text)
			cert := strings.TrimSpace(certValue)
			if cert == "" {
				parts.showToast("Chybí cert")
				return
			}
			cn := extractCN(cert)
			ask := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn)
			proceed := func(final string) {
				if final == "" || final == "Kontakt" || final == "Nový kontakt" {
					final = makeDefault()
				}
				if existing == nil {
					_ = svc.SaveContact(Contact{Name: final, Cert: cert})
				} else {
					c := *existing
					c.Name = final
					c.Cert = cert
					_ = svc.SaveContact(c)
				}
				load()
				apply(strings.TrimSpace(search.Text))
				list.Refresh()
				parts.showToast("Uloženo")
				if useEncrypt {
					parts.peer.SetText(cert)
				}
				if popup != nil {
					popup.Hide()
				}
			}
			if ask {
				entry := widget.NewEntry()
				entry.SetText(name)
				content := container.NewVBox(
					widget.NewLabel(fmt.Sprintf("Common Name nalezen v certifikátu: %s", cn)),
					widget.NewLabel("Chcete použít CN jako název, nebo jej upravit?"),
					entry,
				)
				dialog.NewCustomConfirm("Název kontaktu", "Použít CN", "Uložit", content, func(ok bool) {
					if ok {
						proceed(cn)
						return
					}
					proceed(strings.TrimSpace(entry.Text))
				}, win).Show()
				return
			}
			proceed(name)
		}
		delBtn := widget.NewButtonWithIcon("Smazat", theme.DeleteIcon(), func() {
			if existing == nil {
				popup.Hide()
				return
			}
			dialog.NewConfirm("Smazat", "Opravdu smazat?", func(ok bool) {
				if !ok {
					return
				}
				_ = svc.DeleteContact(existing.ID)
				load()
				apply(strings.TrimSpace(search.Text))
				list.Refresh()
				popup.Hide()
				parts.showToast("Smazáno")
			}, win).Show()
		})
		saveBtn := widget.NewButton("Uložit", func() { save(false) })
		row := container.NewHBox(layout.NewSpacer(), saveBtn, delBtn, layout.NewSpacer())
		title := "Nový kontakt"
		if existing != nil {
			title = "Upravit kontakt"
		}
		// manual entry area below QR for fallback or direct edit
		popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
			widget.NewLabel("Název"), nameEntry,
			widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
			widget.NewSeparator(), row), win)
		popup.Resize(fyne.NewSize(640, 520))
		popup.Show()
	}
	list = widget.NewList(func() int { return len(filtered) }, func() fyne.CanvasObject {
		lbl := widget.NewLabel("")
		msg := widget.NewButton("Zpráva", nil)
		edit := widget.NewButton("Upravit", nil)
		msg.Importance = widget.LowImportance
		edit.Importance = widget.LowImportance
		return container.NewBorder(nil, nil, lbl, container.NewHBox(msg, edit))
	}, func(i widget.ListItemID, o fyne.CanvasObject) {
		if int(i) < 0 || int(i) >= len(filtered) {
			return
		}
		c := filtered[i]
		row := o.(*fyne.Container)
		lbl := row.Objects[0].(*widget.Label)
		btnBox := row.Objects[1].(*fyne.Container)
		msgBtn := btnBox.Objects[0].(*widget.Button)
		editBtn := btnBox.Objects[1].(*widget.Button)
		name := c.Name
		if name == "" {
			name = "(bez názvu)"
		}
		if cn := extractCN(c.Cert); cn != "" {
			lbl.SetText(fmt.Sprintf("%s (%s)", name, cn))
		} else {
			lbl.SetText(name)
		}
		msgBtn.OnTapped = func() { openEncryptPopup(parts, svc, c) }
		editBtn.OnTapped = func() {
			var ptr *Contact
			for i := range all {
				if all[i].ID == c.ID {
					ptr = &all[i]
					break
				}
			}
			openPopup(ptr)
		}
	})
	search.SetPlaceHolder("Hledat…")
	search.OnChanged = func(s string) { apply(s); list.Refresh() }
	addBtn := widget.NewButtonWithIcon("Přidat", theme.ContentAddIcon(), func() { openPopup(nil) })
	header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addBtn)
	load()
	list.Refresh()
	return container.NewBorder(header, nil, nil, nil, container.NewBorder(search, nil, nil, nil, list))
}

type chatScanResult struct {
	direction string
	payload   string
	plaintext string
	err       string
	createdAt time.Time
}

const (
	chatUIDirectionIncoming = "incoming"
	chatUIDirectionOutgoing = "outgoing"
)

func chatBubbleTitle(item chatScanResult) string {
	if item.err != "" {
		return "Zprávu se nepodařilo dešifrovat"
	}
	timestamp := ""
	if !item.createdAt.IsZero() {
		timestamp = " · " + item.createdAt.Local().Format("15:04")
	}
	if item.direction == chatUIDirectionOutgoing {
		return "Odchozí zpráva" + timestamp
	}
	return "Příchozí zpráva" + timestamp
}

func chatBubbleBody(item chatScanResult) string {
	if item.err != "" {
		return item.err
	}
	return item.plaintext
}

func chatBubbleColor(item chatScanResult) color.Color {
	if item.err != "" {
		return chatFailedBubbleColor
	}
	if item.direction == chatUIDirectionOutgoing {
		return chatOutgoingBubbleColor
	}
	return chatIncomingBubbleColor
}

func chatBubbleHeight(item chatScanResult) float32 {
	lineCount := 1
	lineLen := 0
	addLine := func() {
		wrapped := (lineLen + 71) / 72
		lineCount += max(wrapped, 1)
		lineLen = 0
	}
	for _, r := range chatBubbleBody(item) {
		if r == '\n' {
			addLine()
			continue
		}
		lineLen++
	}
	addLine()
	return float32(42 + lineCount*22)
}

func newChatBubbleRow() fyne.CanvasObject {
	background := canvas.NewRectangle(chatIncomingBubbleColor)
	background.CornerRadius = 12
	title := widget.NewLabelWithStyle("", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
	title.Wrapping = fyne.TextWrapWord
	body := widget.NewLabel("")
	body.Wrapping = fyne.TextWrapWord
	return container.NewStack(background, container.NewPadded(container.NewVBox(title, body)))
}

func updateChatBubbleRow(obj fyne.CanvasObject, item chatScanResult) {
	bubble, ok := obj.(*fyne.Container)
	if !ok || len(bubble.Objects) < 2 {
		return
	}
	background, ok := bubble.Objects[0].(*canvas.Rectangle)
	if !ok {
		return
	}
	padded, ok := bubble.Objects[1].(*fyne.Container)
	if !ok || len(padded.Objects) < 1 {
		return
	}
	content, ok := padded.Objects[0].(*fyne.Container)
	if !ok || len(content.Objects) < 2 {
		return
	}
	title, ok := content.Objects[0].(*widget.Label)
	if !ok {
		return
	}
	body, ok := content.Objects[1].(*widget.Label)
	if !ok {
		return
	}

	background.FillColor = chatBubbleColor(item)
	title.SetText(chatBubbleTitle(item))
	body.SetText(chatBubbleBody(item))
	background.Refresh()
	bubble.Refresh()
}

func isEncryptedChatPayload(payload string) bool {
	var envelope hybridEnvelope
	if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
		return false
	}
	return strings.TrimSpace(envelope.EK) != "" &&
		strings.TrimSpace(envelope.N) != "" &&
		strings.TrimSpace(envelope.CT) != ""
}

func buildChatScannerTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
	status := widget.NewLabel("Vyberte oblast obrazovky, nebo spusťte živé sledování chatu.")
	status.Wrapping = fyne.TextWrapWord
	count := widget.NewLabel("0 zpráv")
	var results []chatScanResult
	storedMessages, historyErr := svc.ListChatMessages()
	if historyErr != nil {
		status.SetText("Historii chatu se nepodařilo načíst: " + historyErr.Error())
	} else {
		results = make([]chatScanResult, 0, len(storedMessages))
		for _, message := range storedMessages {
			results = append(results, chatScanResult{
				direction: message.Direction,
				plaintext: message.Text,
				createdAt: message.CreatedAt,
			})
		}
	}
	sort.SliceStable(results, func(i, j int) bool { return results[i].createdAt.Before(results[j].createdAt) })
	var list *widget.List
	var chatListHost *fyne.Container

	newChatList := func() *widget.List {
		chatList := widget.NewList(
			func() int { return len(results) },
			func() fyne.CanvasObject {
				return newChatBubbleRow()
			},
			func(i widget.ListItemID, obj fyne.CanvasObject) {
				if int(i) < 0 || int(i) >= len(results) {
					return
				}
				updateChatBubbleRow(obj, results[i])
			},
		)
		chatList.HideSeparators = true
		return chatList
	}

	list = newChatList()

	refreshChatList := func() {
		if len(results) == 0 {
			list = newChatList()
			if chatListHost != nil {
				chatListHost.Objects = []fyne.CanvasObject{list}
				chatListHost.Refresh()
			}
			count.SetText("0 zpráv")
			return
		}
		for i, item := range results {
			list.SetItemHeight(i, chatBubbleHeight(item))
		}
		list.Refresh()
		count.SetText(fmt.Sprintf("%d zpráv", len(results)))
	}

	decryptPayloads := func(payloads []string) []chatScanResult {
		decoded := make([]chatScanResult, 0, len(payloads))
		for _, payload := range payloads {
			if !isEncryptedChatPayload(payload) {
				continue
			}
			message, err := svc.DecryptChat(payload)
			if err != nil {
				continue
			}
			decoded = append(decoded, chatScanResult{
				direction: message.Direction,
				payload:   payload,
				plaintext: message.Text,
				createdAt: message.CreatedAt,
			})
		}
		return decoded
	}

	persistScannedMessages := func(decoded []chatScanResult) []chatScanResult {
		accepted := make([]chatScanResult, 0, len(decoded))
		for _, item := range decoded {
			message, added, err := svc.AppendScannedChatMessage(item.direction, item.plaintext, item.payload, item.createdAt)
			if err != nil {
				continue
			}
			if !added {
				continue
			}
			item.createdAt = message.CreatedAt
			accepted = append(accepted, item)
		}
		return accepted
	}

	applyResults := func(payloads []string) {
		go func() {
			decoded := decryptPayloads(payloads)
			accepted := persistScannedMessages(decoded)
			fyne.Do(func() {
				results = append(results, accepted...)
				sort.SliceStable(results, func(i, j int) bool { return results[i].createdAt.Before(results[j].createdAt) })
				refreshChatList()
				status.SetText(fmt.Sprintf("Načteno %d QR kódů, úspěšně dešifrováno %d zpráv, přidáno %d nových zpráv.", len(payloads), len(decoded), len(accepted)))
			})
		}()
	}

	decodeImage := func(img image.Image) {
		if img == nil {
			parts.showToast("Žádný obrázek")
			return
		}
		status.SetText("Rozpoznávám QR kódy…")
		go func() {
			payloads, err := DecodeQRCodes(img)
			if err != nil {
				if single, singleErr := DecodeQR(img); singleErr == nil {
					payloads = []string{single}
				} else {
					fyne.Do(func() { status.SetText("QR kód nebyl nalezen: " + err.Error()) })
					return
				}
			}
			applyResults(payloads)
		}()
	}

	scanBtn := widget.NewButtonWithIcon("Vybrat oblast obrazovky", theme.SearchIcon(), func() {
		status.SetText("Čekám na výběr oblasti…")
		go func() {
			img, err := captureScreenRegion()
			if err != nil {
				fyne.Do(func() { status.SetText("Sken obrazovky se nepodařil: " + err.Error()) })
				return
			}
			decodeImage(img)
		}()
	})
	var stopLive func()
	monitorID := 0
	startLiveBtn := widget.NewButtonWithIcon("Spustit živé sledování", theme.MediaPlayIcon(), func() {
		if stopLive != nil {
			return
		}
		monitorID++
		currentMonitorID := monitorID
		stopRequested := make(chan struct{})
		stopLive = func() {
			select {
			case <-stopRequested:
			default:
				close(stopRequested)
			}
			monitorID++
			stopLive = nil
			status.SetText("Živé sledování zastaveno.")
		}
		if strings.Contains(strings.ToLower(os.Getenv("XDG_CURRENT_DESKTOP")), "gnome") {
			status.SetText("V systémovém dialogu vyberte okno nebo monitor s chatem.")
		} else {
			status.SetText("Vybírám oblast pro živé sledování…")
		}
		go func() {
			session, err := openScreenCaptureSession()
			if err != nil {
				fyne.Do(func() {
					status.SetText("Živé sledování se nepodařilo spustit: " + err.Error())
					if monitorID == currentMonitorID {
						stopLive = nil
					}
				})
				return
			}
			defer session.Close()
			ticker := time.NewTicker(time.Second)
			defer ticker.Stop()
			fyne.Do(func() { status.SetText("Živé sledování běží. QR kódy se kontrolují jednou za sekundu.") })
			for {
				select {
				case <-stopRequested:
					return
				case <-ticker.C:
					img, captureErr := session.Capture()
					if captureErr != nil {
						fyne.Do(func() { status.SetText("Snímání selhalo: " + captureErr.Error()) })
						continue
					}
					payloads, decodeErr := DecodeQRCodesFast(img)
					if decodeErr != nil {
						continue
					}
					decoded := persistScannedMessages(decryptPayloads(payloads))
					fyne.Do(func() {
						for _, item := range decoded {
							results = append(results, item)
						}
						sort.SliceStable(results, func(i, j int) bool { return results[i].createdAt.Before(results[j].createdAt) })
						refreshChatList()
					})
				}
			}
		}()
	})
	stopLiveBtn := widget.NewButtonWithIcon("Zastavit", theme.MediaStopIcon(), func() {
		if stopLive != nil {
			stopLive()
		}
	})
	pasteBtn := widget.NewButtonWithIcon("Ze schránky", theme.ContentPasteIcon(), func() {
		img, err := readImageClipboard()
		if err != nil {
			parts.showToast("Chyba schránky: " + err.Error())
			return
		}
		decodeImage(img)
	})
	openBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() {
		win := fyne.CurrentApp().Driver().AllWindows()[0]
		fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
			if err != nil || rc == nil {
				return
			}
			defer rc.Close()
			data, readErr := io.ReadAll(rc)
			if readErr != nil {
				parts.showToast("Chyba čtení souboru")
				return
			}
			img, _, decodeErr := image.Decode(bytes.NewReader(data))
			if decodeErr != nil {
				parts.showToast("Neplatný obrázek: " + decodeErr.Error())
				return
			}
			decodeImage(img)
		}, win)
		fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
		fd.Show()
	})
	clearBtn := widget.NewButtonWithIcon("Skrýt historii", theme.ContentClearIcon(), func() {
		if stopLive != nil {
			stopLive()
		}
		results = nil
		refreshChatList()
		status.SetText("Historie zůstala bezpečně uložená v trezoru a načte se při dalším otevření aplikace.")
	})
	scanToolbar := container.NewVBox(
		container.NewHBox(scanBtn, startLiveBtn, stopLiveBtn),
		container.NewHBox(pasteBtn, openBtn, layout.NewSpacer(), clearBtn),
	)

	debugPayload := widget.NewMultiLineEntry()
	debugPayload.SetPlaceHolder("Sem vložte encrypted payload pro ruční debug dešifrování…")
	debugPayload.SetMinRowsVisible(3)
	debugStatus := widget.NewLabel("")
	debugBtn := widget.NewButtonWithIcon("Dešifrovat payload", theme.ConfirmIcon(), func() {
		payload := strings.TrimSpace(debugPayload.Text)
		if payload == "" {
			debugStatus.SetText("Vložte payload.")
			return
		}
		debugStatus.SetText("Dešifruji…")
		go func() {
			if !isEncryptedChatPayload(payload) {
				fyne.Do(func() { debugStatus.SetText("Tento QR payload není šifrovaná chatová zpráva.") })
				return
			}
			plaintext, err := svc.Decrypt(payload)
			fyne.Do(func() {
				if err != nil {
					debugStatus.SetText("Nelze dešifrovat: " + err.Error())
				} else {
					debugStatus.SetText("Dešifrovaná zpráva (nebyla přidána do historie): " + plaintext)
				}
			})
		}()
	})
	debugPanel := container.NewVBox(
		widget.NewLabelWithStyle("Vývojový debug režim", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		widget.NewLabel("Ručně vložte payload, pokud nelze zprávu načíst ze screenshotu."),
		debugPayload,
		debugBtn,
		debugStatus,
	)
	debugPanel.Hide()
	debugToggle := widget.NewCheck("Zapnout vývojový debug režim", func(enabled bool) {
		if enabled {
			debugPanel.Show()
		} else {
			debugPanel.Hide()
		}
		debugPanel.Refresh()
	})

	messageEntry := widget.NewMultiLineEntry()
	messageEntry.SetMinRowsVisible(3)
	messageEntry.SetPlaceHolder("Napište zprávu, kterou chcete poslat…")
	outgoingQR := canvas.NewImageFromImage(nil)
	outgoingQR.FillMode = canvas.ImageFillContain
	outgoingQR.SetMinSize(fyne.NewSize(200, 200))
	outgoingStatus := widget.NewLabel("QR se zobrazí po zašifrování zprávy.")
	outgoingStatus.Wrapping = fyne.TextWrapWord
	var outgoingPayload string
	var outgoingVersion atomic.Uint64
	var outgoingTimer *time.Timer
	clearOutgoingResult := func() {
		outgoingPayload = ""
		outgoingQR.Image = nil
		outgoingQR.Refresh()
	}

	contactDisplayName := func(contact Contact) string {
		name := strings.TrimSpace(contact.Name)
		if name == "" {
			name = "(bez názvu)"
		}
		if cn := extractCN(contact.Cert); cn != "" && !strings.Contains(name, cn) {
			name = fmt.Sprintf("%s (%s)", name, cn)
		}
		return name
	}
	shortContactID := func(id string) string {
		id = strings.TrimSpace(id)
		if len(id) > 6 {
			return id[:6]
		}
		if id != "" {
			return id
		}
		return "bez-id"
	}

	var contactSelect *widget.Select
	contactByLabel := map[string]Contact{}
	contactLabels := []string{}
	loadContacts := func() {
		contacts, err := svc.ListContacts()
		if err != nil {
			contactByLabel = map[string]Contact{}
			contactLabels = nil
			outgoingStatus.SetText("Kontakty se nepodařilo načíst: " + err.Error())
			return
		}

		baseLabels := make([]string, len(contacts))
		labelCounts := make(map[string]int, len(contacts))
		for i, contact := range contacts {
			label := contactDisplayName(contact)
			baseLabels[i] = label
			labelCounts[label]++
		}

		contactByLabel = make(map[string]Contact, len(contacts))
		contactLabels = make([]string, 0, len(contacts))
		for i, contact := range contacts {
			label := baseLabels[i]
			if labelCounts[label] > 1 {
				label = fmt.Sprintf("%s [%d:%s]", label, i+1, shortContactID(contact.ID))
			}
			contactLabels = append(contactLabels, label)
			contactByLabel[label] = contact
		}
	}
	selectedContactCert := func() (string, bool) {
		if len(contactLabels) == 0 {
			clearOutgoingResult()
			outgoingStatus.SetText("Nejdřív uložte kontakt s certifikátem v záložce Kontakty.")
			return "", false
		}
		if contactSelect == nil || strings.TrimSpace(contactSelect.Selected) == "" {
			clearOutgoingResult()
			outgoingStatus.SetText("Vyberte příjemce ze seznamu kontaktů.")
			return "", false
		}
		contact, ok := contactByLabel[contactSelect.Selected]
		if !ok {
			clearOutgoingResult()
			outgoingStatus.SetText("Vybraný kontakt už není dostupný. Obnovte seznam kontaktů.")
			return "", false
		}
		cert := strings.TrimSpace(contact.Cert)
		if cert == "" {
			clearOutgoingResult()
			outgoingStatus.SetText("Vybraný kontakt nemá uložený certifikát.")
			return "", false
		}
		if _, err := encrypt.ParsePeerPublicKey(cert); err != nil {
			clearOutgoingResult()
			outgoingStatus.SetText("Vybraný kontakt má neplatný certifikát: " + err.Error())
			return "", false
		}
		return cert, true
	}
	var scheduleOutgoingEncryption func()
	validateSelectedContact := func() {
		clearOutgoingResult()
		if cert, ok := selectedContactCert(); ok {
			_ = cert
			outgoingStatus.SetText("Příjemce vybrán. QR se připraví po krátké pauze při psaní.")
		}
	}
	loadContacts()
	contactSelect = widget.NewSelect(contactLabels, func(string) {
		validateSelectedContact()
		if scheduleOutgoingEncryption != nil {
			scheduleOutgoingEncryption()
		}
	})
	contactSelect.PlaceHolder = "Vyberte uložený kontakt"
	if len(contactLabels) == 0 {
		contactSelect.Disable()
		outgoingStatus.SetText("Nejdřív uložte kontakt s certifikátem v záložce Kontakty.")
	} else {
		contactSelect.SetSelected(contactLabels[0])
	}
	refreshContactsBtn := widget.NewButtonWithIcon("Obnovit kontakty", theme.ViewRefreshIcon(), func() {
		previous := contactSelect.Selected
		loadContacts()
		contactSelect.SetOptions(contactLabels)
		if len(contactLabels) == 0 {
			contactSelect.ClearSelected()
			contactSelect.Disable()
			outgoingStatus.SetText("Nejdřív uložte kontakt s certifikátem v záložce Kontakty.")
			return
		}
		contactSelect.Enable()
		if _, ok := contactByLabel[previous]; ok {
			contactSelect.SetSelected(previous)
			return
		}
		contactSelect.SetSelected(contactLabels[0])
	})

	scheduleOutgoingEncryption = func() {
		version := outgoingVersion.Add(1)
		if outgoingTimer != nil {
			outgoingTimer.Stop()
		}
		message := strings.TrimSpace(messageEntry.Text)
		if message == "" {
			clearOutgoingResult()
			outgoingStatus.SetText("Zadejte zprávu.")
			return
		}
		peer, ok := selectedContactCert()
		if !ok {
			return
		}
		outgoingStatus.SetText("QR připravím po krátké pauze…")
		outgoingTimer = time.AfterFunc(450*time.Millisecond, func() {
			payload, err := svc.EncryptChat(message, peer)
			if err != nil {
				fyne.Do(func() {
					if version == outgoingVersion.Load() {
						outgoingStatus.SetText("Šifrování se nepodařilo: " + err.Error())
					}
				})
				return
			}
			data, err := GenerateQRPNG(payload, 640)
			if err != nil {
				fyne.Do(func() {
					if version == outgoingVersion.Load() {
						outgoingStatus.SetText("QR se nepodařilo vytvořit: " + err.Error())
					}
				})
				return
			}
			img, err := LoadPNG(data)
			if err != nil {
				fyne.Do(func() {
					if version == outgoingVersion.Load() {
						outgoingStatus.SetText("QR se nepodařilo načíst: " + err.Error())
					}
				})
				return
			}
			if version != outgoingVersion.Load() {
				return
			}
			fyne.Do(func() {
				if version != outgoingVersion.Load() {
					return
				}
				outgoingPayload = payload
				outgoingQR.Image = img
				outgoingQR.Refresh()
				outgoingStatus.SetText("Hotovo. Zkopírujte QR obrázek a vložte ho do chatu.")
			})
		})
	}
	messageEntry.OnChanged = func(string) { scheduleOutgoingEncryption() }
	copyOutgoingBtn := widget.NewButtonWithIcon("Kopírovat QR pro chat", theme.ContentCopyIcon(), func() {
		copyImageToClipboard(outgoingQR.Image, parts)
	})
	copyPayloadBtn := widget.NewButtonWithIcon("Kopírovat payload", theme.ContentCopyIcon(), func() {
		if outgoingPayload != "" {
			copyClip(outgoingPayload, parts)
		}
	})

	incomingControls := container.NewVBox(
		widget.NewLabelWithStyle("Historie chatu", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		status,
		scanToolbar,
		debugToggle,
		debugPanel,
		container.NewHBox(widget.NewLabelWithStyle("Zprávy", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), count),
	)
	chatListHost = container.NewStack(list)
	incoming := container.NewBorder(incomingControls, nil, nil, nil, chatListHost)
	outgoingForm := container.NewVBox(
		widget.NewLabelWithStyle("Odchozí zpráva", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		widget.NewLabel("Příjemce"),
		container.NewBorder(nil, nil, nil, refreshContactsBtn, contactSelect),
		widget.NewLabel("Zpráva"),
		messageEntry,
		outgoingStatus,
	)
	outgoingPreview := container.NewVBox(
		widget.NewLabel("Výsledný QR kód"),
		container.NewGridWithColumns(2, copyOutgoingBtn, copyPayloadBtn),
		container.NewCenter(outgoingQR),
	)
	outgoing := container.NewScroll(container.NewVBox(outgoingForm, widget.NewSeparator(), outgoingPreview))
	refreshChatList()

	intro := widget.NewLabel("Načtěte QR zprávu přímo z obrazovky. Dešifrovaný text zůstává pouze v tomto trezoru.")
	intro.Wrapping = fyne.TextWrapWord
	header := container.NewVBox(
		widget.NewLabelWithStyle("Skenovat chat", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		intro,
	)
	split := container.NewHSplit(incoming, outgoing)
	split.SetOffset(0.58)
	return container.NewBorder(header, nil, nil, nil, split)
}

func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
	tabs := container.NewAppTabs(
		container.NewTabItem("Skenovat chat", buildChatScannerTab(parts, svc)),
		container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)),
		container.NewTabItem("Kontakty", buildContactsTab(parts, svc)),
		container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)),
	)
	fyne.CurrentApp().Settings().SetTheme(simpleTheme{})
	header := container.NewVBox(
		container.NewHBox(
			widget.NewLabelWithStyle("Fckeuspy", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
			layout.NewSpacer(),
			widget.NewLabel("Lokální šifrovaný chat"),
		),
		widget.NewSeparator(),
	)
	return container.NewBorder(header, parts.toastLabel, nil, nil, tabs)
}
