package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"image"
	"image/color"
	"io"
	"math"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"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/widget"
	"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
	"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
	"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
	"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)

type authorConfig struct {
	OutputType          string  `json:"outputType"`
	Region              string  `json:"region"`
	AspectRatio         string  `json:"aspectRatio"`
	DiscSize            string  `json:"discSize"`
	Title               string  `json:"title"`
	CreateMenu          bool    `json:"createMenu"`
	MenuTemplate        string  `json:"menuTemplate"`
	MenuTheme           string  `json:"menuTheme"`
	MenuBackgroundImage string  `json:"menuBackgroundImage"`
	MenuLogoEnabled     bool    `json:"menuLogoEnabled"`
	MenuLogoPath        string  `json:"menuLogoPath"`
	MenuLogoPosition    string  `json:"menuLogoPosition"`
	MenuLogoScale       float64 `json:"menuLogoScale"`
	MenuLogoMargin      int     `json:"menuLogoMargin"`
	MenuStructure       string  `json:"menuStructure"`
	MenuExtrasEnabled   bool    `json:"menuExtrasEnabled"`
	MenuChapterThumbSrc string  `json:"menuChapterThumbSrc"`
	TreatAsChapters     bool    `json:"treatAsChapters"`
	SceneThreshold      float64 `json:"sceneThreshold"`
}

func defaultAuthorConfig() authorConfig {
	return authorConfig{
		OutputType:          "dvd",
		Region:              "AUTO",
		AspectRatio:         "AUTO",
		DiscSize:            "DVD5",
		Title:               "",
		CreateMenu:          false,
		MenuTemplate:        "Simple",
		MenuTheme:           "VideoTools",
		MenuBackgroundImage: "",
		MenuLogoEnabled:     true,
		MenuLogoPath:        "",
		MenuLogoPosition:    "Top Right",
		MenuLogoScale:       1.0,
		MenuLogoMargin:      24,
		MenuStructure:       "Feature + Chapters",
		MenuExtrasEnabled:   false,
		MenuChapterThumbSrc: "Auto",
		TreatAsChapters:     false,
		SceneThreshold:      0.3,
	}
}

func loadPersistedAuthorConfig() (authorConfig, error) {
	var cfg authorConfig
	path := moduleConfigPath("author")
	data, err := os.ReadFile(path)
	if err != nil {
		return cfg, err
	}
	if err := json.Unmarshal(data, &cfg); err != nil {
		return cfg, err
	}
	if cfg.OutputType == "" {
		cfg.OutputType = "dvd"
	}
	if cfg.Region == "" {
		cfg.Region = "AUTO"
	}
	if cfg.AspectRatio == "" {
		cfg.AspectRatio = "AUTO"
	}
	if cfg.DiscSize == "" {
		cfg.DiscSize = "DVD5"
	}
	if cfg.MenuTemplate == "" {
		cfg.MenuTemplate = "Simple"
	}
	if cfg.MenuTheme == "" {
		cfg.MenuTheme = "VideoTools"
	}
	if cfg.MenuLogoPosition == "" {
		cfg.MenuLogoPosition = "Top Right"
	}
	if cfg.MenuLogoScale == 0 {
		cfg.MenuLogoScale = 1.0
	}
	if cfg.MenuLogoMargin == 0 {
		cfg.MenuLogoMargin = 24
	}
	if cfg.MenuStructure == "" {
		cfg.MenuStructure = "Feature + Chapters"
	}
	if cfg.MenuChapterThumbSrc == "" {
		cfg.MenuChapterThumbSrc = "Auto"
	}
	if cfg.SceneThreshold <= 0 {
		cfg.SceneThreshold = 0.3
	}
	return cfg, nil
}

func savePersistedAuthorConfig(cfg authorConfig) error {
	path := moduleConfigPath("author")
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

func (s *appState) applyAuthorConfig(cfg authorConfig) {
	s.authorOutputType = cfg.OutputType
	s.authorRegion = cfg.Region
	s.authorAspectRatio = cfg.AspectRatio
	s.authorDiscSize = cfg.DiscSize
	s.authorTitle = cfg.Title
	s.authorCreateMenu = cfg.CreateMenu
	s.authorMenuTemplate = cfg.MenuTemplate
	s.authorMenuTheme = cfg.MenuTheme
	s.authorMenuBackgroundImage = cfg.MenuBackgroundImage
	s.authorMenuLogoEnabled = cfg.MenuLogoEnabled
	s.authorMenuLogoPath = cfg.MenuLogoPath
	s.authorMenuLogoPosition = cfg.MenuLogoPosition
	s.authorMenuLogoScale = cfg.MenuLogoScale
	s.authorMenuLogoMargin = cfg.MenuLogoMargin
	s.authorMenuStructure = cfg.MenuStructure
	s.authorMenuExtrasEnabled = cfg.MenuExtrasEnabled
	s.authorMenuChapterThumbSrc = cfg.MenuChapterThumbSrc
	s.authorTreatAsChapters = cfg.TreatAsChapters
	s.authorSceneThreshold = cfg.SceneThreshold
}

func (s *appState) persistAuthorConfig() {
	cfg := authorConfig{
		OutputType:          s.authorOutputType,
		Region:              s.authorRegion,
		AspectRatio:         s.authorAspectRatio,
		DiscSize:            s.authorDiscSize,
		Title:               s.authorTitle,
		CreateMenu:          s.authorCreateMenu,
		MenuTemplate:        s.authorMenuTemplate,
		MenuTheme:           s.authorMenuTheme,
		MenuBackgroundImage: s.authorMenuBackgroundImage,
		MenuLogoEnabled:     s.authorMenuLogoEnabled,
		MenuLogoPath:        s.authorMenuLogoPath,
		MenuLogoPosition:    s.authorMenuLogoPosition,
		MenuLogoScale:       s.authorMenuLogoScale,
		MenuLogoMargin:      s.authorMenuLogoMargin,
		MenuStructure:       s.authorMenuStructure,
		MenuExtrasEnabled:   s.authorMenuExtrasEnabled,
		MenuChapterThumbSrc: s.authorMenuChapterThumbSrc,
		TreatAsChapters:     s.authorTreatAsChapters,
		SceneThreshold:      s.authorSceneThreshold,
	}
	if err := savePersistedAuthorConfig(cfg); err != nil {
		logging.Debug(logging.CatSystem, "failed to persist author config: %v", err)
	}
}

func buildAuthorView(state *appState) fyne.CanvasObject {
	state.stopPreview()
	state.lastModule = state.active
	state.active = "author"

	if cfg, err := loadPersistedAuthorConfig(); err == nil {
		state.applyAuthorConfig(cfg)
	}

	if state.authorOutputType == "" {
		state.authorOutputType = "dvd"
	}
	if state.authorRegion == "" {
		state.authorRegion = "AUTO"
	}
	if state.authorAspectRatio == "" {
		state.authorAspectRatio = "AUTO"
	}
	if state.authorDiscSize == "" {
		state.authorDiscSize = "DVD5"
	}
	if state.authorMenuTemplate == "" {
		state.authorMenuTemplate = "Simple"
	}
	if state.authorMenuTheme == "" {
		state.authorMenuTheme = "VideoTools"
	}
	if state.authorMenuLogoPosition == "" {
		state.authorMenuLogoPosition = "Top Right"
	}
	if state.authorMenuLogoScale == 0 {
		state.authorMenuLogoScale = 1.0
	}
	if state.authorMenuLogoMargin == 0 {
		state.authorMenuLogoMargin = 24
	}
	if state.authorMenuStructure == "" {
		state.authorMenuStructure = "Feature + Chapters"
	}
	if state.authorMenuChapterThumbSrc == "" {
		state.authorMenuChapterThumbSrc = "Auto"
	}

	authorColor := moduleColor("author")

	backBtn := widget.NewButton("< BACK", func() {
		state.showMainMenu()
	})
	backBtn.Importance = widget.LowImportance

	queueBtn := widget.NewButton("View Queue", func() {
		state.showQueue()
	})
	state.queueBtn = queueBtn
	state.updateQueueButtonLabel()

	clearCompletedBtn := widget.NewButton("⌫", func() {
		state.clearCompletedJobs()
	})
	clearCompletedBtn.Importance = widget.LowImportance

	cancelBtn := widget.NewButton("Cancel Job", func() {
		if state.jobQueue != nil {
			if job := state.jobQueue.CurrentRunning(); job != nil && job.Type == queue.JobTypeAuthor {
				state.jobQueue.Cancel(job.ID)
			}
		}
	})
	cancelBtn.Importance = widget.DangerImportance
	state.authorCancelBtn = cancelBtn
	state.updateAuthorCancelButton()

	topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), cancelBtn, clearCompletedBtn, queueBtn))
	bottomBar := moduleFooter(authorColor, layout.NewSpacer(), state.statsBar)

	tabs := container.NewAppTabs(
		container.NewTabItem("Videos", buildVideoClipsTab(state)),
		container.NewTabItem("Chapters", buildChaptersTab(state)),
		container.NewTabItem("Subtitles", buildSubtitlesTab(state)),
		container.NewTabItem("Menu", buildAuthorMenuTab(state)),
		container.NewTabItem("Settings", buildAuthorSettingsTab(state)),
		container.NewTabItem("Generate", buildAuthorDiscTab(state)),
	)
	tabs.SetTabLocation(container.TabLocationTop)

	return container.NewBorder(topBar, bottomBar, nil, nil, tabs)
}

func buildVideoClipsTab(state *appState) fyne.CanvasObject {
	state.authorVideoTSPath = strings.TrimSpace(state.authorVideoTSPath)
	list := container.NewVBox()
	listScroll := container.NewVScroll(list)

	var rebuildList func()
	var emptyOverlay *fyne.Container
	rebuildList = func() {
		list.Objects = nil

		// Show VIDEO_TS folder if loaded
		if state.authorVideoTSPath != "" {
			if emptyOverlay != nil {
				emptyOverlay.Hide()
			}

			videoTSLabel := widget.NewLabel("VIDEO_TS Folder:")
			videoTSLabel.TextStyle = fyne.TextStyle{Bold: true}
			pathLabel := widget.NewLabel(state.authorVideoTSPath)
			pathLabel.Wrapping = fyne.TextWrapBreak

			removeBtn := widget.NewButton("Remove", func() {
				state.authorVideoTSPath = ""
				state.authorOutputType = "dvd"
				rebuildList()
				state.updateAuthorSummary()
			})
			removeBtn.Importance = widget.MediumImportance

			infoLabel := widget.NewLabel("This VIDEO_TS folder will be burned directly to ISO without re-encoding.")
			infoLabel.TextStyle = fyne.TextStyle{Italic: true}
			infoLabel.Wrapping = fyne.TextWrapWord

			row := container.NewBorder(
				nil,
				nil,
				nil,
				removeBtn,
				container.NewVBox(videoTSLabel, pathLabel, infoLabel),
			)
			cardBg := canvas.NewRectangle(utils.MustHex("#171C2A"))
			cardBg.CornerRadius = 6
			cardBg.SetMinSize(fyne.NewSize(0, videoTSLabel.MinSize().Height+pathLabel.MinSize().Height+infoLabel.MinSize().Height+20))
			list.Add(container.NewPadded(container.NewMax(cardBg, row)))
			list.Refresh()
			return
		}

		if len(state.authorClips) == 0 {
			if emptyOverlay != nil {
				emptyOverlay.Show()
			}
			list.Refresh()
			return
		}

		if emptyOverlay != nil {
			emptyOverlay.Hide()
		}
		for i, clip := range state.authorClips {
			idx := i
			nameLabel := widget.NewLabel(clip.DisplayName)
			nameLabel.TextStyle = fyne.TextStyle{Bold: true}
			durationLabel := widget.NewLabel(fmt.Sprintf("%.2fs", clip.Duration))
			durationLabel.TextStyle = fyne.TextStyle{Italic: true}
			durationLabel.Alignment = fyne.TextAlignTrailing

			titleEntry := widget.NewEntry()
			titleEntry.SetPlaceHolder(fmt.Sprintf("Chapter %d", idx+1))
			titleEntry.SetText(clip.ChapterTitle)
			titleEntry.OnChanged = func(val string) {
				state.authorClips[idx].ChapterTitle = val
				if state.authorTreatAsChapters {
					state.authorChapters = chaptersFromClips(state.authorClips)
					state.authorChapterSource = "clips"
					state.updateAuthorSummary()
				}
			}

			// Note about chapter names for future menu support
			noteLabel := widget.NewLabel("(For future DVD menus)")
			noteLabel.TextStyle = fyne.TextStyle{Italic: true}
			noteLabel.Alignment = fyne.TextAlignLeading

			removeBtn := widget.NewButton("Remove", func() {
				state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
				rebuildList()
				state.updateAuthorSummary()
			})
			removeBtn.Importance = widget.MediumImportance

			row := container.NewBorder(
				nil,
				nil,
				nil,
				container.NewVBox(durationLabel, removeBtn),
				container.NewVBox(nameLabel, titleEntry, noteLabel),
			)
			cardBg := canvas.NewRectangle(utils.MustHex("#171C2A"))
			cardBg.CornerRadius = 6
			cardBg.SetMinSize(fyne.NewSize(0, nameLabel.MinSize().Height+durationLabel.MinSize().Height+12))
			list.Add(container.NewPadded(container.NewMax(cardBg, row)))
		}
		list.Refresh()
	}

	addBtn := widget.NewButton("Add Files", func() {
		dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
			if err != nil || reader == nil {
				return
			}
			defer reader.Close()
			state.addAuthorFiles([]string{reader.URI().Path()})
			rebuildList()
		}, state.window)
	})
	addBtn.Importance = widget.HighImportance

	clearBtn := widget.NewButton("Clear All", func() {
		state.authorClips = []authorClip{}
		state.authorChapters = nil
		state.authorChapterSource = ""
		state.authorVideoTSPath = ""
		state.authorTitle = ""
		rebuildList()
		state.updateAuthorSummary()
	})
	clearBtn.Importance = widget.MediumImportance

	addQueueBtn := widget.NewButton("Add to Queue", func() {
		if len(state.authorClips) == 0 {
			dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
			return
		}
		state.startAuthorGeneration(false)
	})
	addQueueBtn.Importance = widget.MediumImportance

	compileBtn := widget.NewButton("COMPILE TO DVD", func() {
		if len(state.authorClips) == 0 {
			dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
			return
		}
		state.startAuthorGeneration(true)
	})
	compileBtn.Importance = widget.HighImportance

	chapterToggle := widget.NewCheck("Treat videos as chapters", func(checked bool) {
		state.authorTreatAsChapters = checked
		if checked {
			state.authorChapters = chaptersFromClips(state.authorClips)
			state.authorChapterSource = "clips"
		} else if state.authorChapterSource == "clips" {
			state.authorChapterSource = ""
			state.authorChapters = nil
		}
		state.updateAuthorSummary()
		state.persistAuthorConfig()
		if state.authorChaptersRefresh != nil {
			state.authorChaptersRefresh()
		}
	})
	chapterToggle.SetChecked(state.authorTreatAsChapters)

	dropTarget := ui.NewDroppable(listScroll, func(items []fyne.URI) {
		var paths []string
		for _, uri := range items {
			if uri.Scheme() == "file" {
				paths = append(paths, uri.Path())
			}
		}
		if len(paths) > 0 {
			state.addAuthorFiles(paths)
			rebuildList()
		}
	})

	emptyLabel := widget.NewLabel("Drag and drop video files or VIDEO_TS folder here\nor click 'Add Files' to select videos")
	emptyLabel.Alignment = fyne.TextAlignCenter
	emptyOverlay = container.NewCenter(emptyLabel)

	listArea := container.NewMax(dropTarget, emptyOverlay)

	// DVD Title entry (synced with Settings tab)
	dvdTitleEntry := widget.NewEntry()
	dvdTitleEntry.SetPlaceHolder("DVD Title")
	dvdTitleEntry.SetText(state.authorTitle)
	dvdTitleEntry.OnChanged = func(value string) {
		state.authorTitle = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	}

	// Note about chapter names
	chapterNote := widget.NewLabel("Chapter names are saved for future DVD menu support")
	chapterNote.TextStyle = fyne.TextStyle{Italic: true}

	controls := container.NewBorder(
		container.NewVBox(
			widget.NewLabel("DVD Title:"),
			dvdTitleEntry,
			widget.NewSeparator(),
			widget.NewLabel("Videos:"),
			chapterNote,
		),
		container.NewVBox(chapterToggle, container.NewHBox(addBtn, clearBtn, addQueueBtn, compileBtn)),
		nil,
		nil,
		listArea,
	)

	rebuildList()
	return container.NewPadded(controls)
}

func buildChaptersTab(state *appState) fyne.CanvasObject {
	var fileLabel *widget.Label
	if state.authorFile != nil {
		fileLabel = widget.NewLabel(fmt.Sprintf("File: %s", filepath.Base(state.authorFile.Path)))
		fileLabel.TextStyle = fyne.TextStyle{Bold: true}
	} else {
		fileLabel = widget.NewLabel("Select a single video file or use clips from Videos tab")
	}

	chapterList := container.NewVBox()
	sourceLabel := widget.NewLabel("")
	refreshChapters := func() {
		chapterList.Objects = nil
		sourceLabel.SetText("")
		if len(state.authorChapters) == 0 {
			if state.authorTreatAsChapters && len(state.authorClips) > 1 {
				state.authorChapters = chaptersFromClips(state.authorClips)
				state.authorChapterSource = "clips"
			}
		}
		if len(state.authorChapters) == 0 {
			chapterList.Add(widget.NewLabel("No chapters detected yet"))
			return
		}
		switch state.authorChapterSource {
		case "clips":
			sourceLabel.SetText("Source: Video clips (treat as chapters)")
		case "embedded":
			sourceLabel.SetText("Source: Embedded chapters")
		case "scenes":
			sourceLabel.SetText("Source: Scene detection")
		case "videots":
			sourceLabel.SetText("Source: VIDEO_TS chapters")
		default:
			sourceLabel.SetText("Source: Chapters")
		}
		for i, ch := range state.authorChapters {
			title := ch.Title
			if title == "" {
				title = fmt.Sprintf("Chapter %d", i+1)
			}
			chapterList.Add(widget.NewLabel(fmt.Sprintf("%02d. %s (%s)", i+1, title, formatChapterTime(ch.Timestamp))))
		}
	}
	state.authorChaptersRefresh = refreshChapters

	selectBtn := widget.NewButton("Select Video", func() {
		dialog.ShowFileOpen(func(uc fyne.URIReadCloser, err error) {
			if err != nil || uc == nil {
				return
			}
			defer uc.Close()
			path := uc.URI().Path()
			src, err := probeVideo(path)
			if err != nil {
				dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
				return
			}
			state.authorFile = src
			fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(src.Path)))
			// Clear the custom title so it can be re-derived from the new content.
			// This addresses the user's request for the title to "reset".
			state.authorTitle = ""
			state.updateAuthorSummary()
			// Update the UI for the title entry if the settings tab is currently visible.
			if state.active == "author" && state.window.Canvas() != nil {
				app := fyne.CurrentApp()
				if app != nil && app.Driver() != nil {
					app.Driver().DoFromGoroutine(func() {
						state.showAuthorView() // Rebuild the module to refresh titleEntry
					}, false)
				}
			}
			state.loadEmbeddedChapters(path)
			refreshChapters()
		}, state.window)
	})

	thresholdLabel := widget.NewLabel(fmt.Sprintf("Detection Sensitivity: %.2f", state.authorSceneThreshold))
	thresholdSlider := widget.NewSlider(0.1, 0.9)
	thresholdSlider.Value = state.authorSceneThreshold
	thresholdSlider.Step = 0.05
	thresholdSlider.OnChanged = func(v float64) {
		state.authorSceneThreshold = v
		thresholdLabel.SetText(fmt.Sprintf("Detection Sensitivity: %.2f", v))
		state.persistAuthorConfig()
	}

	detectBtn := widget.NewButton("Detect Scenes", func() {
		targetPath := ""
		if state.authorFile != nil {
			targetPath = state.authorFile.Path
		} else if len(state.authorClips) > 0 {
			targetPath = state.authorClips[0].Path
		}
		if targetPath == "" {
			dialog.ShowInformation("No File", "Please select a video file first", state.window)
			return
		}

		progress := dialog.NewProgressInfinite("Scene Detection", "Analyzing scene changes with FFmpeg...", state.window)
		progress.Show()
		state.authorDetecting = true

		go func() {
			chapters, err := detectSceneChapters(targetPath, state.authorSceneThreshold)
			runOnUI(func() {
				progress.Hide()
				state.authorDetecting = false
				if err != nil {
					dialog.ShowError(err, state.window)
					return
				}
				if len(chapters) == 0 {
					dialog.ShowInformation("Scene Detection", "No scene changes detected at the current sensitivity.", state.window)
					return
				}
				// Show chapter preview dialog for visual verification
				state.showChapterPreview(targetPath, chapters, func(accepted bool) {
					if accepted {
						state.authorChapters = chapters
						state.authorChapterSource = "scenes"
						state.updateAuthorSummary()
						refreshChapters()
					}
				})
			})
		}()
	})
	detectBtn.Importance = widget.HighImportance

	addChapterBtn := widget.NewButton("+ Add Chapter", func() {
		dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented.", state.window)
	})

	exportBtn := widget.NewButton("Export Chapters", func() {
		dialog.ShowInformation("Export", "Chapter export will be implemented", state.window)
	})

	controlsTop := container.NewVBox(
		fileLabel,
		selectBtn,
		widget.NewSeparator(),
		widget.NewLabel("Scene Detection:"),
		thresholdLabel,
		thresholdSlider,
		detectBtn,
		widget.NewSeparator(),
		widget.NewLabel("Chapters:"),
		sourceLabel,
	)

	listScroll := container.NewScroll(chapterList)
	bottomRow := container.NewHBox(addChapterBtn, exportBtn)

	controls := container.NewBorder(
		controlsTop,
		bottomRow,
		nil,
		nil,
		listScroll,
	)

	refreshChapters()
	return container.NewPadded(controls)
}

func buildSubtitlesTab(state *appState) fyne.CanvasObject {
	list := container.NewVBox()
	listScroll := container.NewVScroll(list)

	var buildSubList func()
	var emptyOverlay *fyne.Container
	buildSubList = func() {
		list.Objects = nil

		if len(state.authorSubtitles) == 0 {
			if emptyOverlay != nil {
				emptyOverlay.Show()
			}
			list.Refresh()
			return
		}

		if emptyOverlay != nil {
			emptyOverlay.Hide()
		}
		for i, path := range state.authorSubtitles {
			idx := i
			card := widget.NewCard(filepath.Base(path), "", nil)

			removeBtn := widget.NewButton("Remove", func() {
				state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
				buildSubList()
				state.updateAuthorSummary()
			})
			removeBtn.Importance = widget.MediumImportance

			cardContent := container.NewVBox(removeBtn)
			card.SetContent(cardContent)
			list.Add(card)
		}
		list.Refresh()
	}

	addBtn := widget.NewButton("Add Subtitles", func() {
		dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
			if err != nil || reader == nil {
				return
			}
			defer reader.Close()
			state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
			buildSubList()
			state.updateAuthorSummary()
		}, state.window)
	})
	addBtn.Importance = widget.HighImportance

	openSubtitlesBtn := widget.NewButton("Open Subtitles Tool", func() {
		if state.authorFile != nil {
			state.subtitleVideoPath = state.authorFile.Path
		} else if len(state.authorClips) > 0 {
			state.subtitleVideoPath = state.authorClips[0].Path
		}
		if len(state.authorSubtitles) > 0 {
			state.subtitleFilePath = state.authorSubtitles[0]
		}
		state.showSubtitlesView()
	})
	openSubtitlesBtn.Importance = widget.MediumImportance

	clearBtn := widget.NewButton("Clear All", func() {
		state.authorSubtitles = []string{}
		buildSubList()
		state.updateAuthorSummary()
	})
	clearBtn.Importance = widget.MediumImportance

	dropTarget := ui.NewDroppable(listScroll, func(items []fyne.URI) {
		var paths []string
		for _, uri := range items {
			if uri.Scheme() == "file" {
				paths = append(paths, uri.Path())
			}
		}
		if len(paths) > 0 {
			state.authorSubtitles = append(state.authorSubtitles, paths...)
			buildSubList()
			state.updateAuthorSummary()
		}
	})

	emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
	emptyLabel.Alignment = fyne.TextAlignCenter
	emptyOverlay = container.NewCenter(emptyLabel)

	listArea := container.NewMax(dropTarget, emptyOverlay)

	controls := container.NewBorder(
		widget.NewLabel("Subtitle Tracks:"),
		container.NewHBox(addBtn, openSubtitlesBtn, clearBtn),
		nil,
		nil,
		listArea,
	)

	buildSubList()
	return container.NewPadded(controls)
}

func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
	outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"}, func(value string) {
		if value == "DVD (VIDEO_TS)" {
			state.authorOutputType = "dvd"
		} else {
			state.authorOutputType = "iso"
		}
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorOutputType == "iso" {
		outputType.SetSelected("ISO Image")
	} else {
		outputType.SetSelected("DVD (VIDEO_TS)")
	}

	regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
		state.authorRegion = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorRegion == "" {
		regionSelect.SetSelected("AUTO")
	} else {
		regionSelect.SetSelected(state.authorRegion)
	}

	aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
		state.authorAspectRatio = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorAspectRatio == "" {
		aspectSelect.SetSelected("AUTO")
	} else {
		aspectSelect.SetSelected(state.authorAspectRatio)
	}

	titleEntry := widget.NewEntry()
	titleEntry.SetPlaceHolder("DVD Title")
	titleEntry.SetText(state.authorTitle)
	titleEntry.OnChanged = func(value string) {
		state.authorTitle = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	}

	discSizeSelect := widget.NewSelect([]string{"DVD5", "DVD9"}, func(value string) {
		state.authorDiscSize = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorDiscSize == "" {
		discSizeSelect.SetSelected("DVD5")
	} else {
		discSizeSelect.SetSelected(state.authorDiscSize)
	}

	applyControls := func() {
		if state.authorOutputType == "iso" {
			outputType.SetSelected("ISO Image")
		} else {
			outputType.SetSelected("DVD (VIDEO_TS)")
		}
		if state.authorRegion == "" {
			regionSelect.SetSelected("AUTO")
		} else {
			regionSelect.SetSelected(state.authorRegion)
		}
		if state.authorAspectRatio == "" {
			aspectSelect.SetSelected("AUTO")
		} else {
			aspectSelect.SetSelected(state.authorAspectRatio)
		}
		if state.authorDiscSize == "" {
			discSizeSelect.SetSelected("DVD5")
		} else {
			discSizeSelect.SetSelected(state.authorDiscSize)
		}
		titleEntry.SetText(state.authorTitle)
	}

	loadCfgBtn := widget.NewButton("Load Config", func() {
		cfg, err := loadPersistedAuthorConfig()
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", state.window)
			} else {
				dialog.ShowError(fmt.Errorf("failed to load config: %w", err), state.window)
			}
			return
		}
		state.applyAuthorConfig(cfg)
		applyControls()
		state.updateAuthorSummary()
	})

	saveCfgBtn := widget.NewButton("Save Config", func() {
		cfg := authorConfig{
			OutputType:          state.authorOutputType,
			Region:              state.authorRegion,
			AspectRatio:         state.authorAspectRatio,
			DiscSize:            state.authorDiscSize,
			Title:               state.authorTitle,
			CreateMenu:          state.authorCreateMenu,
			MenuTemplate:        state.authorMenuTemplate,
			MenuTheme:           state.authorMenuTheme,
			MenuBackgroundImage: state.authorMenuBackgroundImage,
			MenuLogoEnabled:     state.authorMenuLogoEnabled,
			MenuLogoPath:        state.authorMenuLogoPath,
			MenuLogoPosition:    state.authorMenuLogoPosition,
			MenuLogoScale:       state.authorMenuLogoScale,
			MenuLogoMargin:      state.authorMenuLogoMargin,
			MenuStructure:       state.authorMenuStructure,
			MenuExtrasEnabled:   state.authorMenuExtrasEnabled,
			MenuChapterThumbSrc: state.authorMenuChapterThumbSrc,
			TreatAsChapters:     state.authorTreatAsChapters,
			SceneThreshold:      state.authorSceneThreshold,
		}
		if err := savePersistedAuthorConfig(cfg); err != nil {
			dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
			return
		}
		dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", moduleConfigPath("author")), state.window)
	})

	resetBtn := widget.NewButton("Reset", func() {
		cfg := defaultAuthorConfig()
		state.applyAuthorConfig(cfg)
		applyControls()
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})

	info := widget.NewLabel("Requires: ffmpeg, dvdauthor, and mkisofs/genisoimage (for ISO).")
	info.Wrapping = fyne.TextWrapWord

	controls := container.NewVBox(
		widget.NewLabel("Output Settings:"),
		widget.NewSeparator(),
		widget.NewLabel("Output Type:"),
		outputType,
		widget.NewLabel("Region:"),
		regionSelect,
		widget.NewLabel("Aspect Ratio:"),
		aspectSelect,
		widget.NewLabel("Disc Size:"),
		discSizeSelect,
		widget.NewLabel("DVD Title:"),
		titleEntry,
		widget.NewSeparator(),
		info,
		widget.NewSeparator(),
		container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn),
	)

	return container.NewPadded(controls)
}

func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
	navyBlue := utils.MustHex("#191F35")
	boxAccent := gridColor
	var updateMenuControls func(bool)

	buildMenuBox := func(title string, content fyne.CanvasObject) fyne.CanvasObject {
		bg := canvas.NewRectangle(navyBlue)
		bg.CornerRadius = 10
		bg.StrokeColor = boxAccent
		bg.StrokeWidth = 1
		body := container.NewVBox(
			widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
			widget.NewSeparator(),
			content,
		)
		return container.NewMax(bg, container.NewPadded(body))
	}

	sectionGap := func() fyne.CanvasObject {
		gap := canvas.NewRectangle(color.Transparent)
		gap.SetMinSize(fyne.NewSize(0, 10))
		return gap
	}

	createMenuCheck := widget.NewCheck("Enable DVD Menus", func(checked bool) {
		state.authorCreateMenu = checked
		state.updateAuthorSummary()
		state.persistAuthorConfig()
		if updateMenuControls != nil {
			updateMenuControls(checked)
		}
	})
	createMenuCheck.SetChecked(state.authorCreateMenu)
	menuDisabledNote := widget.NewLabel("DVD menus will not be generated unless enabled.")
	menuDisabledNote.TextStyle = fyne.TextStyle{Italic: true}
	menuDisabledNote.Wrapping = fyne.TextWrapWord

	menuThemeSelect := widget.NewSelect([]string{"VideoTools"}, func(value string) {
		state.authorMenuTheme = value
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorMenuTheme == "" {
		state.authorMenuTheme = "VideoTools"
	}
	menuThemeSelect.SetSelected(state.authorMenuTheme)

	templateOptions := []string{
		"Simple (Title + Buttons)",
		"Poster (Grid Thumbnails)",
		"Classic (Title + Buttons)",
	}
	templateValueByLabel := map[string]string{
		"Simple (Title + Buttons)":  "Simple",
		"Poster (Grid Thumbnails)":  "Poster",
		"Classic (Title + Buttons)": "Dark",
	}
	templateLabelByValue := map[string]string{
		"Simple": "Simple (Title + Buttons)",
		"Poster": "Poster (Grid Thumbnails)",
		"Dark":   "Classic (Title + Buttons)",
	}

	menuTemplateSelect := widget.NewSelect(templateOptions, func(value string) {
		state.authorMenuTemplate = templateValueByLabel[value]
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})
	if state.authorMenuTemplate == "" {
		state.authorMenuTemplate = "Simple"
	}
	templateLabel := templateLabelByValue[state.authorMenuTemplate]
	if templateLabel == "" {
		templateLabel = templateOptions[0]
		state.authorMenuTemplate = templateValueByLabel[templateLabel]
	}
	menuTemplateSelect.SetSelected(templateLabel)

	bgImageLabel := widget.NewLabel(state.authorMenuBackgroundImage)
	bgImageLabel.Wrapping = fyne.TextWrapWord
	bgImageButton := widget.NewButton("Select Background Image", func() {
		dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
			if err != nil || reader == nil {
				return
			}
			defer reader.Close()
			state.authorMenuBackgroundImage = reader.URI().Path()
			bgImageLabel.SetText(state.authorMenuBackgroundImage)
			state.updateAuthorSummary()
			state.persistAuthorConfig()
		}, state.window)
	})
	bgImageButton.Importance = widget.HighImportance
	bgImageButton.Hidden = state.authorMenuTemplate != "Poster"
	bgImageLabel.Hidden = state.authorMenuTemplate != "Poster"

	menuTemplateSelect.OnChanged = func(value string) {
		state.authorMenuTemplate = templateValueByLabel[value]
		showPoster := state.authorMenuTemplate == "Poster"
		bgImageButton.Hidden = !showPoster
		bgImageLabel.Hidden = !showPoster
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	}

	logoEnableCheck := widget.NewCheck("Embed Logo", nil)
	logoEnableCheck.SetChecked(state.authorMenuLogoEnabled)

	logoFileEntry := widget.NewEntry()
	logoFileEntry.Disable()
	logoFileEntry.SetPlaceHolder("No logo selected")
	logoPreview := canvas.NewImageFromFile("")
	logoPreview.FillMode = canvas.ImageFillContain
	logoPreview.SetMinSize(fyne.NewSize(96, 96))
	logoPreviewLabel := widget.NewLabel("No logo selected")
	logoPreviewLabel.Wrapping = fyne.TextWrapWord
	logoPreviewSize := widget.NewLabel("")
	logoPreviewSize.Wrapping = fyne.TextWrapWord
	logoPreviewBorder := canvas.NewRectangle(color.NRGBA{R: 80, G: 86, B: 100, A: 120})
	logoPreviewBorder.SetMinSize(fyne.NewSize(120, 96))
	logoPreviewBox := container.NewMax(
		logoPreviewBorder,
		container.NewPadded(container.NewCenter(logoPreview)),
	)
	previewMin := canvas.NewRectangle(color.NRGBA{A: 0})
	previewMin.SetMinSize(fyne.NewSize(220, 0))
	updateBrandingTitle := func() {}

	menuPreviewSize := func() (int, int) {
		width := 720
		height := 480
		switch strings.ToUpper(strings.TrimSpace(state.authorRegion)) {
		case "PAL":
			height = 576
		}
		return width, height
	}

	logoDisplayName := func() string {
		if strings.TrimSpace(state.authorMenuLogoPath) == "" {
			return "VT_Logo.png (default)"
		}
		return filepath.Base(state.authorMenuLogoPath)
	}

	updateLogoPreview := func() {
		logoFileEntry.SetText(logoDisplayName())
		if !state.authorMenuLogoEnabled {
			logoPreviewBox.Hide()
			logoPreviewLabel.SetText("Logo disabled")
			logoPreviewSize.SetText("")
			return
		}

		path := state.authorMenuLogoPath
		if strings.TrimSpace(path) == "" {
			path = filepath.Join("assets", "logo", "VT_Logo.png")
		}

		if _, err := os.Stat(path); err != nil {
			logoPreviewBox.Hide()
			logoPreviewLabel.SetText("Logo file not found")
			logoPreviewSize.SetText("")
			return
		}

		logoPreviewBox.Show()
		logoPreviewLabel.SetText(filepath.Base(path))
		logoPreview.File = path
		logoPreview.Refresh()

		file, err := os.Open(path)
		if err != nil {
			logoPreviewSize.SetText("")
			return
		}
		defer file.Close()

		cfg, _, err := image.DecodeConfig(file)
		if err != nil {
			logoPreviewSize.SetText("")
			return
		}

		menuW, menuH := menuPreviewSize()
		maxW := int(float64(menuW) * 0.25)
		maxH := int(float64(menuH) * 0.25)
		scale := state.authorMenuLogoScale
		targetW := int(math.Round(float64(cfg.Width) * scale))
		targetH := int(math.Round(float64(cfg.Height) * scale))
		if targetW > maxW || targetH > maxH {
			ratioW := float64(maxW) / float64(targetW)
			ratioH := float64(maxH) / float64(targetH)
			ratio := math.Min(ratioW, ratioH)
			targetW = int(math.Round(float64(targetW) * ratio))
			targetH = int(math.Round(float64(targetH) * ratio))
		}

		logoPreviewSize.SetText(fmt.Sprintf("Logo size: %dx%d (max %dx%d)", targetW, targetH, maxW, maxH))
	}
	logoPickButton := widget.NewButton("Select Logo", func() {
		dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
			if err != nil || reader == nil {
				return
			}
			defer reader.Close()
			state.authorMenuLogoPath = reader.URI().Path()
			logoFileEntry.SetText(logoDisplayName())
			updateLogoPreview()
			updateBrandingTitle()
			state.updateAuthorSummary()
			state.persistAuthorConfig()
		}, state.window)
	})
	logoPickButton.Importance = widget.MediumImportance
	logoClearButton := widget.NewButton("Clear", func() {
		state.authorMenuLogoPath = ""
		logoFileEntry.SetText(logoDisplayName())
		logoEnableCheck.SetChecked(false)
		updateLogoPreview()
		updateBrandingTitle()
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	})

	logoPositionSelect := widget.NewSelect([]string{
		"Top Left",
		"Top Right",
		"Bottom Left",
		"Bottom Right",
		"Center",
	}, func(value string) {
		state.authorMenuLogoPosition = value
		updateBrandingTitle()
		state.persistAuthorConfig()
	})
	if state.authorMenuLogoPosition == "" {
		state.authorMenuLogoPosition = "Top Right"
	}
	logoPositionSelect.SetSelected(state.authorMenuLogoPosition)

	scaleOptions := []string{"50%", "75%", "100%", "125%", "150%", "200%"}
	scaleValueByLabel := map[string]float64{
		"50%":  0.5,
		"75%":  0.75,
		"100%": 1.0,
		"125%": 1.25,
		"150%": 1.5,
		"200%": 2.0,
	}
	scaleLabelByValue := map[float64]string{
		0.5:  "50%",
		0.75: "75%",
		1.0:  "100%",
		1.25: "125%",
		1.5:  "150%",
		2.0:  "200%",
	}
	if state.authorMenuLogoScale == 0 {
		state.authorMenuLogoScale = 1.0
	}
	logoScaleSelect := widget.NewSelect(scaleOptions, func(value string) {
		if scale, ok := scaleValueByLabel[value]; ok {
			state.authorMenuLogoScale = scale
			updateLogoPreview()
			updateBrandingTitle()
			state.persistAuthorConfig()
		}
	})
	scaleLabel := scaleLabelByValue[state.authorMenuLogoScale]
	if scaleLabel == "" {
		scaleLabel = "100%"
		state.authorMenuLogoScale = 1.0
	}
	logoScaleSelect.SetSelected(scaleLabel)

	if state.authorMenuLogoMargin == 0 {
		state.authorMenuLogoMargin = 24
	}
	marginEntry := widget.NewEntry()
	marginEntry.SetText(strconv.Itoa(state.authorMenuLogoMargin))
	updatingMargin := false
	updateMargin := func(value int, updateEntry bool) {
		if value < 0 {
			value = 0
		}
		if value > 60 {
			value = 60
		}
		state.authorMenuLogoMargin = value
		if updateEntry {
			updatingMargin = true
			marginEntry.SetText(strconv.Itoa(value))
			updatingMargin = false
		}
		state.persistAuthorConfig()
	}
	marginEntry.OnChanged = func(value string) {
		if updatingMargin {
			return
		}
		if strings.TrimSpace(value) == "" {
			return
		}
		if v, err := strconv.Atoi(value); err == nil {
			if v == state.authorMenuLogoMargin {
				return
			}
			updateMargin(v, true)
		}
	}
	marginMinus := widget.NewButton("-", func() {
		updateMargin(state.authorMenuLogoMargin-2, true)
	})
	marginPlus := widget.NewButton("+", func() {
		updateMargin(state.authorMenuLogoMargin+2, true)
	})

	safeAreaNote := widget.NewLabel("Logos are constrained to DVD safe areas.")
	safeAreaNote.TextStyle = fyne.TextStyle{Italic: true}
	safeAreaNote.Wrapping = fyne.TextWrapWord

	menuStructureSelect := widget.NewSelect([]string{
		"Feature Only",
		"Feature + Chapters",
		"Feature + Extras",
		"Feature + Chapters + Extras",
	}, func(value string) {
		state.authorMenuStructure = value
		state.persistAuthorConfig()
	})
	if state.authorMenuStructure == "" {
		state.authorMenuStructure = "Feature + Chapters"
	}
	menuStructureSelect.SetSelected(state.authorMenuStructure)

	extrasMenuCheck := widget.NewCheck("Enable Extras Menu", func(checked bool) {
		state.authorMenuExtrasEnabled = checked
		state.persistAuthorConfig()
	})
	extrasMenuCheck.SetChecked(state.authorMenuExtrasEnabled)
	extrasNote := widget.NewLabel("Videos marked as Extras appear in a separate menu.")
	extrasNote.Wrapping = fyne.TextWrapWord

	thumbSourceSelect := widget.NewSelect([]string{
		"Auto",
		"First Frame",
		"Midpoint",
		"Custom (Advanced)",
	}, func(value string) {
		state.authorMenuChapterThumbSrc = value
		state.persistAuthorConfig()
	})
	if state.authorMenuChapterThumbSrc == "" {
		state.authorMenuChapterThumbSrc = "Auto"
	}
	thumbSourceSelect.SetSelected(state.authorMenuChapterThumbSrc)

	info := widget.NewLabel("DVD menus are generated using the VideoTools theme and IBM Plex Mono. Menu settings apply only to disc authoring.")
	info.Wrapping = fyne.TextWrapWord

	logoPreviewGroup := container.NewMax(
		previewMin,
		container.NewVBox(
			widget.NewLabel("Preview:"),
			logoPreviewLabel,
			logoPreviewBox,
			logoPreviewSize,
		),
	)

	logoButtonRow := container.NewHBox(
		logoPickButton,
		logoClearButton,
	)
	logoFileRow := container.NewBorder(nil, nil, nil, logoButtonRow, logoFileEntry)

	logoPositionRow := container.NewHBox(
		widget.NewLabel("Position:"),
		layout.NewSpacer(),
		logoPositionSelect,
	)
	logoScaleRow := container.NewHBox(
		widget.NewLabel("Scale:"),
		layout.NewSpacer(),
		logoScaleSelect,
	)
	logoMarginRow := container.NewHBox(
		widget.NewLabel("Margin:"),
		marginMinus,
		marginEntry,
		marginPlus,
		widget.NewLabel("px"),
	)

	menuCore := buildMenuBox("Menu Core", container.NewVBox(
		createMenuCheck,
		menuDisabledNote,
		widget.NewLabel("Theme:"),
		menuThemeSelect,
		widget.NewLabel("Template:"),
		menuTemplateSelect,
		bgImageLabel,
		bgImageButton,
		widget.NewLabel("Menu Structure:"),
		menuStructureSelect,
	))

	brandingLeft := container.NewVBox(
		logoEnableCheck,
		widget.NewLabel("Logo File:"),
		logoFileRow,
		logoPositionRow,
		logoScaleRow,
		logoMarginRow,
		safeAreaNote,
	)
	brandingContent := container.NewBorder(nil, nil, nil, logoPreviewGroup, brandingLeft)
	brandingItem := widget.NewAccordionItem("Branding (Logo Overlay)", brandingContent)
	brandingItem.Open = false
	brandingAccordion := widget.NewAccordion(brandingItem)
	branding := container.NewMax(
		canvas.NewRectangle(navyBlue),
		container.NewPadded(brandingAccordion),
	)
	if bg, ok := branding.Objects[0].(*canvas.Rectangle); ok {
		bg.CornerRadius = 10
		bg.StrokeColor = boxAccent
		bg.StrokeWidth = 1
	}

	navigation := buildMenuBox("Navigation", container.NewVBox(
		extrasMenuCheck,
		extrasNote,
		widget.NewLabel("Chapter Thumbnail Source:"),
		thumbSourceSelect,
	))

	controls := container.NewVBox(
		menuCore,
		sectionGap(),
		branding,
		sectionGap(),
		navigation,
		sectionGap(),
		info,
	)

	updateMenuControls = func(enabled bool) {
		menuDisabledNote.Hidden = enabled
		setEnabled := func(on bool, items ...fyne.Disableable) {
			for _, item := range items {
				if on {
					item.Enable()
				} else {
					item.Disable()
				}
			}
		}

		setEnabled(enabled,
			menuThemeSelect,
			menuTemplateSelect,
			bgImageButton,
			menuStructureSelect,
			logoEnableCheck,
			logoPickButton,
			logoClearButton,
			logoPositionSelect,
			logoScaleSelect,
			marginEntry,
			marginMinus,
			marginPlus,
			extrasMenuCheck,
			thumbSourceSelect,
		)

		logoControlsEnabled := enabled && state.authorMenuLogoEnabled
		setEnabled(logoControlsEnabled,
			logoPickButton,
			logoClearButton,
			logoPositionSelect,
			logoScaleSelect,
			marginEntry,
			marginMinus,
			marginPlus,
		)
	}

	updateBrandingTitle = func() {
		if !state.authorMenuLogoEnabled {
			brandingItem.Title = "Branding: Disabled"
			brandingAccordion.Refresh()
			return
		}
		scaleText := scaleLabelByValue[state.authorMenuLogoScale]
		if scaleText == "" {
			scaleText = "100%"
		}
		name := logoDisplayName()
		brandingItem.Title = fmt.Sprintf("Branding: %s (%s, %s)", name, state.authorMenuLogoPosition, scaleText)
		brandingAccordion.Refresh()
	}

	logoEnableCheck.OnChanged = func(checked bool) {
		state.authorMenuLogoEnabled = checked
		updateLogoPreview()
		updateBrandingTitle()
		updateMenuControls(state.authorCreateMenu)
		state.updateAuthorSummary()
		state.persistAuthorConfig()
	}

	updateLogoPreview()
	updateBrandingTitle()
	updateMenuControls(state.authorCreateMenu)

	scroll := container.NewVScroll(container.NewPadded(controls))
	scroll.SetMinSize(fyne.NewSize(0, 420))
	return scroll
}

func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
	generateBtn := widget.NewButton("GENERATE DVD", func() {
		if len(state.authorClips) == 0 && state.authorFile == nil {
			dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
			return
		}
		state.startAuthorGeneration(true)
	})
	generateBtn.Importance = widget.HighImportance

	summaryLabel := widget.NewLabel(authorSummary(state))
	summaryLabel.Wrapping = fyne.TextWrapWord
	state.authorSummaryLabel = summaryLabel

	statusLabel := widget.NewLabel("Ready")
	statusLabel.Wrapping = fyne.TextWrapWord
	state.authorStatusLabel = statusLabel

	progressBar := widget.NewProgressBar()
	progressBar.SetValue(state.authorProgress / 100.0)
	state.authorProgressBar = progressBar

	logEntry := widget.NewMultiLineEntry()
	logEntry.Wrapping = fyne.TextWrapOff
	logEntry.Disable()
	logEntry.SetText(state.authorLogText)
	state.authorLogEntry = logEntry
	logScroll := container.NewVScroll(logEntry)
	logScroll.SetMinSize(fyne.NewSize(0, 200))
	state.authorLogScroll = logScroll

	// Log control buttons
	copyLogBtn := widget.NewButton("Copy Log", func() {
		if state.authorLogFilePath != "" {
			// Copy from file for accuracy
			if data, err := os.ReadFile(state.authorLogFilePath); err == nil {
				state.window.Clipboard().SetContent(string(data))
				dialog.ShowInformation("Copied", "Full authoring log copied to clipboard", state.window)
				return
			}
		}
		// Fallback to in-memory log
		state.window.Clipboard().SetContent(state.authorLogText)
		dialog.ShowInformation("Copied", "Authoring log copied to clipboard", state.window)
	})
	copyLogBtn.Importance = widget.LowImportance

	viewFullLogBtn := widget.NewButton("View Full Log", func() {
		if state.authorLogFilePath == "" || state.authorLogFilePath == "-" {
			dialog.ShowInformation("No Log File", "No log file available to view", state.window)
			return
		}
		if _, err := os.Stat(state.authorLogFilePath); err != nil {
			dialog.ShowError(fmt.Errorf("log file not found: %w", err), state.window)
			return
		}
		state.openLogViewer("Authoring Log", state.authorLogFilePath, false)
	})
	viewFullLogBtn.Importance = widget.LowImportance

	logControls := container.NewHBox(
		widget.NewLabel("Authoring Log (last 100 lines):"),
		layout.NewSpacer(),
		copyLogBtn,
		viewFullLogBtn,
	)

	controls := container.NewVBox(
		widget.NewLabel("Generate DVD/ISO:"),
		widget.NewSeparator(),
		summaryLabel,
		widget.NewSeparator(),
		widget.NewLabel("Status:"),
		statusLabel,
		progressBar,
		widget.NewSeparator(),
		logControls,
		logScroll,
		widget.NewSeparator(),
		generateBtn,
	)

	return container.NewPadded(controls)
}

func authorSummary(state *appState) string {
	summary := "Ready to generate:\n\n"
	if state.authorVideoTSPath != "" {
		summary += fmt.Sprintf("VIDEO_TS: %s\n", filepath.Base(filepath.Dir(state.authorVideoTSPath)))
	} else if len(state.authorClips) > 0 {
		summary += fmt.Sprintf("Videos: %d\n", len(state.authorClips))
		for i, clip := range state.authorClips {
			summary += fmt.Sprintf("  %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
		}
	} else if state.authorFile != nil {
		summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
	}

	if len(state.authorSubtitles) > 0 {
		summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
		for i, path := range state.authorSubtitles {
			summary += fmt.Sprintf("  %d. %s\n", i+1, filepath.Base(path))
		}
	}

	if count, label := state.authorChapterSummary(); count > 0 {
		summary += fmt.Sprintf("%s: %d\n", label, count)
	}

	summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
	summary += fmt.Sprintf("Disc Size: %s\n", state.authorDiscSize)
	summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
	summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
	if outPath := authorDefaultOutputPath(state.authorOutputType, authorOutputTitle(state), authorSummaryPaths(state)); outPath != "" {
		summary += fmt.Sprintf("Output Path: %s\n", outPath)
	}
	if state.authorTitle != "" {
		summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
	}
	if totalDur := authorTotalDuration(state); totalDur > 0 {
		bitrate := authorTargetBitrateKbps(state.authorDiscSize, totalDur)
		summary += fmt.Sprintf("Estimated Target Bitrate: %dkbps\n", bitrate)
	}
	return summary
}

func (s *appState) addAuthorFiles(paths []string) {
	wasEmpty := len(s.authorClips) == 0
	for _, path := range paths {
		src, err := probeVideo(path)
		if err != nil {
			dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
			continue
		}

		clip := authorClip{
			Path:         path,
			DisplayName:  filepath.Base(path),
			Duration:     src.Duration,
			Chapters:     []authorChapter{},
			ChapterTitle: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
		}
		s.authorClips = append(s.authorClips, clip)
	}

	if wasEmpty && len(s.authorClips) == 1 {
		s.loadEmbeddedChapters(s.authorClips[0].Path)
	} else if len(s.authorClips) > 1 && s.authorChapterSource == "embedded" {
		s.authorChapters = nil
		s.authorChapterSource = ""
	}
	s.authorTitle = ""
	s.updateAuthorSummary()
	// Update the UI for the title entry if the settings tab is currently visible.
	// This ensures the title entry visually resets as well.
	if s.active == "author" && s.window.Canvas() != nil {
		app := fyne.CurrentApp()
		if app != nil && app.Driver() != nil {
			app.Driver().DoFromGoroutine(func() {
				// Rebuild the settings tab to refresh its controls.
				// This is a bit heavy, but ensures the titleEntry reflects the change.
				s.showAuthorView()
			}, false)
		}
	}
}

func (s *appState) updateAuthorSummary() {
	if s.authorSummaryLabel == nil {
		return
	}
	s.authorSummaryLabel.SetText(authorSummary(s))
}

func (s *appState) authorChapterSummary() (int, string) {
	if len(s.authorChapters) > 0 {
		switch s.authorChapterSource {
		case "embedded":
			return len(s.authorChapters), "Embedded Chapters"
		case "scenes":
			return len(s.authorChapters), "Scene Chapters"
		case "videots":
			return len(s.authorChapters), "VIDEO_TS Chapters"
		default:
			return len(s.authorChapters), "Chapters"
		}
	}
	if s.authorTreatAsChapters && len(s.authorClips) > 1 {
		return len(s.authorClips), "Clip Chapters"
	}
	return 0, ""
}

func authorTotalDuration(state *appState) float64 {
	if len(state.authorClips) > 0 {
		var total float64
		for _, clip := range state.authorClips {
			total += clip.Duration
		}
		return total
	}
	if state.authorFile != nil {
		return state.authorFile.Duration
	}
	return 0
}

func authorSummaryPaths(state *appState) []string {
	if state.authorVideoTSPath != "" {
		return []string{state.authorVideoTSPath}
	}
	if len(state.authorClips) > 0 {
		paths := make([]string, 0, len(state.authorClips))
		for _, clip := range state.authorClips {
			paths = append(paths, clip.Path)
		}
		return paths
	}
	if state.authorFile != nil {
		return []string{state.authorFile.Path}
	}
	return nil
}

func authorOutputTitle(state *appState) string {
	title := strings.TrimSpace(state.authorTitle)
	if title != "" {
		return title
	}
	if state.authorVideoTSPath != "" {
		return filepath.Base(filepath.Dir(state.authorVideoTSPath))
	}
	return defaultAuthorTitle(authorSummaryPaths(state))
}

func authorTargetBitrateKbps(discSize string, totalSeconds float64) int {
	if totalSeconds <= 0 {
		return 0
	}
	var targetBytes float64
	switch strings.ToUpper(strings.TrimSpace(discSize)) {
	case "DVD9":
		targetBytes = 7.3 * 1024 * 1024 * 1024
	default:
		targetBytes = 4.1 * 1024 * 1024 * 1024
	}
	totalBits := targetBytes * 8
	kbps := int(totalBits / totalSeconds / 1000)
	if kbps > 9500 {
		kbps = 9500
	}
	if kbps < 1500 {
		kbps = 1500
	}
	return kbps
}

func (s *appState) loadEmbeddedChapters(path string) {
	chapters, err := extractChaptersFromFile(path)
	if err != nil || len(chapters) == 0 {
		if s.authorChapterSource == "embedded" {
			s.authorChapters = nil
			s.authorChapterSource = ""
			s.updateAuthorSummary()
			if s.authorChaptersRefresh != nil {
				s.authorChaptersRefresh()
			}
		}
		return
	}
	s.authorChapters = chapters
	s.authorChapterSource = "embedded"
	s.updateAuthorSummary()
	if s.authorChaptersRefresh != nil {
		s.authorChaptersRefresh()
	}
}

func (s *appState) loadVideoTSChapters(videoTSPath string) {
	chapters, err := extractChaptersFromVideoTS(videoTSPath)
	if err != nil || len(chapters) == 0 {
		// No chapters found, clear if previously set
		if s.authorChapterSource == "videots" {
			s.authorChapters = nil
			s.authorChapterSource = ""
			s.updateAuthorSummary()
			if s.authorChaptersRefresh != nil {
				s.authorChaptersRefresh()
			}
		}
		return
	}
	s.authorChapters = chapters
	s.authorChapterSource = "videots"
	s.updateAuthorSummary()
	if s.authorChaptersRefresh != nil {
		s.authorChaptersRefresh()
	}
}

func chaptersFromClips(clips []authorClip) []authorChapter {
	if len(clips) == 0 {
		return nil
	}
	var chapters []authorChapter
	var t float64
	firstTitle := strings.TrimSpace(clips[0].ChapterTitle)
	if firstTitle == "" {
		firstTitle = "Chapter 1"
	}
	chapters = append(chapters, authorChapter{Timestamp: 0, Title: firstTitle, Auto: true})
	for i := 1; i < len(clips); i++ {
		t += clips[i-1].Duration
		title := strings.TrimSpace(clips[i].ChapterTitle)
		if title == "" {
			title = fmt.Sprintf("Chapter %d", i+1)
		}
		chapters = append(chapters, authorChapter{
			Timestamp: t,
			Title:     title,
			Auto:      true,
		})
	}
	return chapters
}

func detectSceneChapters(path string, threshold float64) ([]authorChapter, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
	defer cancel()

	filter := fmt.Sprintf("select='gt(scene,%.2f)',showinfo", threshold)
	cmd := utils.CreateCommand(ctx, utils.GetFFmpegPath(),
		"-hide_banner",
		"-loglevel", "info",
		"-i", path,
		"-vf", filter,
		"-an",
		"-f", "null",
		"-",
	)
	out, err := cmd.CombinedOutput()
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}

	times := map[float64]struct{}{}
	scanner := bufio.NewScanner(bytes.NewReader(out))
	for scanner.Scan() {
		line := scanner.Text()
		idx := strings.Index(line, "pts_time:")
		if idx == -1 {
			continue
		}
		rest := line[idx+len("pts_time:"):]
		end := strings.IndexAny(rest, " ")
		if end == -1 {
			end = len(rest)
		}
		valStr := strings.TrimSpace(rest[:end])
		if valStr == "" {
			continue
		}
		if val, err := utils.ParseFloat(valStr); err == nil {
			times[val] = struct{}{}
		}
	}

	var vals []float64
	for v := range times {
		if v < 0.01 {
			continue
		}
		vals = append(vals, v)
	}
	sort.Float64s(vals)

	if len(vals) == 0 {
		if err != nil {
			return nil, fmt.Errorf("scene detection failed: %s", strings.TrimSpace(string(out)))
		}
		return nil, nil
	}

	chapters := []authorChapter{{Timestamp: 0, Title: "Chapter 1", Auto: true}}
	for i, v := range vals {
		chapters = append(chapters, authorChapter{
			Timestamp: v,
			Title:     fmt.Sprintf("Chapter %d", i+2),
			Auto:      true,
		})
	}
	return chapters, nil
}

func extractChaptersFromFile(path string) ([]authorChapter, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cmd := utils.CreateCommand(ctx, utils.GetFFprobePath(),
		"-v", "quiet",
		"-print_format", "json",
		"-show_chapters",
		path,
	)
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	var result struct {
		Chapters []struct {
			StartTime string                 `json:"start_time"`
			Tags      map[string]interface{} `json:"tags"`
		} `json:"chapters"`
	}
	if err := json.Unmarshal(out, &result); err != nil {
		return nil, err
	}

	var chapters []authorChapter
	for i, ch := range result.Chapters {
		t, err := utils.ParseFloat(ch.StartTime)
		if err != nil {
			continue
		}
		title := ""
		if ch.Tags != nil {
			if v, ok := ch.Tags["title"]; ok {
				title = fmt.Sprintf("%v", v)
			}
		}
		if title == "" {
			title = fmt.Sprintf("Chapter %d", i+1)
		}
		chapters = append(chapters, authorChapter{
			Timestamp: t,
			Title:     title,
			Auto:      true,
		})
	}

	return chapters, nil
}

func extractChaptersFromVideoTS(videoTSPath string) ([]authorChapter, error) {
	logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: searching for VOB files in: %s", videoTSPath)

	// Try to find the main title VOB files
	// Usually VTS_01_1.VOB contains the main content
	vobFiles, err := filepath.Glob(filepath.Join(videoTSPath, "VTS_*_1.VOB"))
	if err != nil {
		logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: glob error: %v", err)
		return nil, fmt.Errorf("error searching for VOB files: %w", err)
	}
	if len(vobFiles) == 0 {
		logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: no VTS_*_1.VOB files found")
		return nil, fmt.Errorf("no VOB files found in VIDEO_TS")
	}

	logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: found %d VOB files: %v", len(vobFiles), vobFiles)

	// Sort to get the first title set (usually the main feature)
	sort.Strings(vobFiles)
	mainVOB := vobFiles[0]

	logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: using main VOB: %s", mainVOB)

	// Try to extract chapters from the main VOB using ffprobe
	chapters, err := extractChaptersFromFile(mainVOB)
	if err != nil {
		logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: ffprobe error: %v", err)
		return nil, err
	}

	logging.Debug(logging.CatModule, "extractChaptersFromVideoTS: extracted %d chapters", len(chapters))
	return chapters, nil
}

func chaptersToDVDAuthor(chapters []authorChapter) string {
	if len(chapters) == 0 {
		return ""
	}
	var times []float64
	for _, ch := range chapters {
		if ch.Timestamp < 0 {
			continue
		}
		times = append(times, ch.Timestamp)
	}
	if len(times) == 0 {
		return ""
	}
	sort.Float64s(times)
	if times[0] > 0.01 {
		times = append([]float64{0}, times...)
	}
	seen := map[int]struct{}{}
	var parts []string
	for _, t := range times {
		key := int(t * 1000)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		parts = append(parts, formatChapterTime(t))
	}
	return strings.Join(parts, ",")
}

func formatChapterTime(sec float64) string {
	if sec < 0 {
		sec = 0
	}
	d := time.Duration(sec * float64(time.Second))
	h := int(d.Hours())
	m := int(d.Minutes()) % 60
	s := int(d.Seconds()) % 60
	return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
}

func concatDVDMpg(inputs []string, output string) error {
	listPath := filepath.Join(filepath.Dir(output), "concat_list.txt")
	listFile, err := os.Create(listPath)
	if err != nil {
		return fmt.Errorf("failed to create concat list: %w", err)
	}
	for _, path := range inputs {
		fmt.Fprintf(listFile, "file '%s'\n", strings.ReplaceAll(path, "'", "'\\''"))
	}
	if err := listFile.Close(); err != nil {
		return fmt.Errorf("failed to write concat list: %w", err)
	}
	defer os.Remove(listPath)

	args := []string{
		"-hide_banner",
		"-loglevel", "error",
		"-f", "concat",
		"-safe", "0",
		"-i", listPath,
		"-c", "copy",
		"-f", "dvd", // Maintain DVD format
		"-muxrate", "10080000", // DVD mux rate
		"-packetsize", "2048", // DVD packet size
		output,
	}
	cmd := utils.CreateCommandRaw(utils.GetFFmpegPath(), args...)
	return cmd.Run()
}

func (s *appState) resetAuthorLog() {
	s.authorLogText = ""
	s.authorLogLines = nil
	s.authorLogFilePath = ""
	if s.authorLogEntry != nil {
		s.authorLogEntry.SetText("")
	}
	if s.authorLogScroll != nil {
		s.authorLogScroll.ScrollToTop()
	}
}

func (s *appState) appendAuthorLog(line string) {
	if strings.TrimSpace(line) == "" {
		return
	}

	// Keep only last 100 lines for UI display (tail behavior)
	const maxLines = 100
	s.authorLogLines = append(s.authorLogLines, line)
	if len(s.authorLogLines) > maxLines {
		s.authorLogLines = s.authorLogLines[len(s.authorLogLines)-maxLines:]
	}

	// Rebuild text from buffer
	s.authorLogText = strings.Join(s.authorLogLines, "\n")

	if s.authorLogEntry != nil {
		s.authorLogEntry.SetText(s.authorLogText)
	}
	if s.authorLogScroll != nil {
		s.authorLogScroll.ScrollToBottom()
	}
}

func (s *appState) setAuthorStatus(text string) {
	if text == "" {
		text = "Ready"
	}
	if s.authorStatusLabel != nil {
		s.authorStatusLabel.SetText(text)
	}
}

func (s *appState) setAuthorProgress(percent float64) {
	if percent < 0 {
		percent = 0
	}
	if percent > 100 {
		percent = 100
	}
	s.authorProgress = percent
	if s.authorProgressBar != nil {
		s.authorProgressBar.SetValue(percent / 100.0)
	}
}

func (s *appState) updateAuthorCancelButton() {
	if s.authorCancelBtn == nil {
		return
	}
	if s.jobQueue == nil {
		s.authorCancelBtn.Hide()
		return
	}
	job := s.jobQueue.CurrentRunning()
	if job != nil && job.Type == queue.JobTypeAuthor {
		s.authorCancelBtn.Show()
	} else {
		s.authorCancelBtn.Hide()
	}
}

func (s *appState) startAuthorGeneration(startNow bool) {
	if s.authorVideoTSPath != "" {
		title := authorOutputTitle(s)
		outputPath := authorDefaultOutputPath("iso", title, []string{s.authorVideoTSPath})
		if outputPath == "" {
			dialog.ShowError(fmt.Errorf("failed to resolve output path"), s.window)
			return
		}
		if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, startNow); err != nil {
			dialog.ShowError(err, s.window)
		}
		return
	}

	paths, primary, err := s.authorSourcePaths()
	if err != nil {
		dialog.ShowError(err, s.window)
		return
	}

	region := resolveAuthorRegion(s.authorRegion, primary)
	aspect := resolveAuthorAspect(s.authorAspectRatio, primary)
	title := strings.TrimSpace(s.authorTitle)
	if title == "" {
		title = defaultAuthorTitle(paths)
	}

	warnings := authorWarnings(s)
	uiCall := func(fn func()) {
		app := fyne.CurrentApp()
		if app != nil && app.Driver() != nil {
			app.Driver().DoFromGoroutine(fn, false)
			return
		}
		fn()
	}
	continuePrompt := func() {
		uiCall(func() {
			s.promptAuthorOutput(paths, region, aspect, title, startNow)
		})
	}
	if len(warnings) > 0 {
		uiCall(func() {
			dialog.ShowConfirm("Authoring Notes", strings.Join(warnings, "\n")+"\n\nContinue?", func(ok bool) {
				if ok {
					continuePrompt()
				}
			}, s.window)
		})
		return
	}

	continuePrompt()
}

func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string, startNow bool) {
	outputType := strings.ToLower(strings.TrimSpace(s.authorOutputType))
	if outputType == "" {
		outputType = "dvd"
	}

	outputPath := authorDefaultOutputPath(outputType, title, paths)
	if outputType == "iso" {
		s.generateAuthoring(paths, region, aspect, title, outputPath, true, startNow)
		return
	}
	s.generateAuthoring(paths, region, aspect, title, outputPath, false, startNow)
}

func authorWarnings(state *appState) []string {
	var warnings []string
	if len(state.authorSubtitles) > 0 {
		warnings = append(warnings, "Subtitle tracks are not authored yet; they will be ignored.")
	}
	if len(state.authorAudioTracks) > 0 {
		warnings = append(warnings, "Additional audio tracks are not authored yet; they will be ignored.")
	}
	if totalDur := authorTotalDuration(state); totalDur > 0 {
		bitrate := authorTargetBitrateKbps(state.authorDiscSize, totalDur)
		if bitrate < 3000 {
			warnings = append(warnings, fmt.Sprintf("Long runtime detected; target bitrate ~%dkbps may reduce quality.", bitrate))
		}
	}
	return warnings
}

func (s *appState) authorSourcePaths() ([]string, *videoSource, error) {
	if len(s.authorClips) > 0 {
		paths := make([]string, 0, len(s.authorClips))
		for _, clip := range s.authorClips {
			paths = append(paths, clip.Path)
		}
		primary, err := probeVideo(paths[0])
		if err != nil {
			return nil, nil, fmt.Errorf("failed to probe source: %w", err)
		}
		return paths, primary, nil
	}

	if s.authorFile != nil {
		return []string{s.authorFile.Path}, s.authorFile, nil
	}

	return nil, nil, fmt.Errorf("no authoring content selected")
}

func resolveAuthorRegion(pref string, src *videoSource) string {
	pref = strings.ToUpper(strings.TrimSpace(pref))
	if pref == "NTSC" || pref == "PAL" {
		return pref
	}
	if src != nil {
		if src.FrameRate > 0 {
			if src.FrameRate <= 26 {
				return "PAL"
			}
			return "NTSC"
		}
		if src.Height == 576 {
			return "PAL"
		}
		if src.Height == 480 {
			return "NTSC"
		}
	}
	return "NTSC"
}

func resolveAuthorAspect(pref string, src *videoSource) string {
	pref = strings.TrimSpace(pref)
	if pref == "4:3" || pref == "16:9" {
		return pref
	}
	if src != nil && src.Width > 0 && src.Height > 0 {
		ratio := float64(src.Width) / float64(src.Height)
		if ratio >= 1.55 {
			return "16:9"
		}
		return "4:3"
	}
	return "16:9"
}

func defaultAuthorTitle(paths []string) string {
	if len(paths) == 0 {
		return "DVD"
	}
	base := filepath.Base(paths[0])
	return strings.TrimSuffix(base, filepath.Ext(base))
}

func authorOutputFolderName(title string, paths []string) string {
	name := strings.TrimSpace(title)
	if name == "" {
		name = defaultAuthorTitle(paths)
	}
	name = sanitizeForPath(name)
	if name == "" {
		name = "dvd_output"
	}
	return name
}

func authorDefaultOutputDir(outputType string) string {
	home, err := os.UserHomeDir()
	if err != nil || home == "" {
		home = "."
	}
	dir := filepath.Join(home, "Videos", "VideoTools")
	if strings.EqualFold(outputType, "iso") {
		return filepath.Join(dir, "ISO_Convert")
	}
	return filepath.Join(dir, "DVD_Convert")
}

func authorDefaultOutputPath(outputType, title string, paths []string) string {
	outputType = strings.ToLower(strings.TrimSpace(outputType))
	if outputType == "" {
		outputType = "dvd"
	}
	baseDir := authorDefaultOutputDir(outputType)
	name := strings.TrimSpace(title)
	if name == "" {
		name = defaultAuthorTitle(paths)
	}
	name = sanitizeForPath(name)
	if name == "" {
		name = "dvd_output"
	}
	if outputType == "iso" {
		return uniqueFilePath(filepath.Join(baseDir, name+".iso"))
	}
	return uniqueFolderPath(filepath.Join(baseDir, name))
}

func authorTempRoot(outputPath string) string {
	trimmed := strings.TrimSpace(outputPath)
	if trimmed == "" {
		return utils.TempDir()
	}
	lower := strings.ToLower(trimmed)
	root := trimmed
	if strings.HasSuffix(lower, ".iso") {
		root = filepath.Dir(trimmed)
	} else if ext := filepath.Ext(trimmed); ext != "" {
		root = filepath.Dir(trimmed)
	}
	if root == "" || root == "." {
		return utils.TempDir()
	}
	return root
}

func uniqueFolderPath(path string) string {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return path
	}
	for i := 1; i < 1000; i++ {
		tryPath := fmt.Sprintf("%s-%d", path, i)
		if _, err := os.Stat(tryPath); os.IsNotExist(err) {
			return tryPath
		}
	}
	return fmt.Sprintf("%s-%d", path, time.Now().Unix())
}

func uniqueFilePath(path string) string {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return path
	}
	ext := filepath.Ext(path)
	base := strings.TrimSuffix(path, ext)
	for i := 1; i < 1000; i++ {
		tryPath := fmt.Sprintf("%s-%d%s", base, i, ext)
		if _, err := os.Stat(tryPath); os.IsNotExist(err) {
			return tryPath
		}
	}
	return fmt.Sprintf("%s-%d%s", base, time.Now().Unix(), ext)
}

func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO, startNow bool) {
	if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, startNow); err != nil {
		dialog.ShowError(err, s.window)
	}
}

func (s *appState) addAuthorToQueue(paths []string, region, aspect, title, outputPath string, makeISO bool, startNow bool) error {
	if s.jobQueue == nil {
		return fmt.Errorf("queue not initialized")
	}

	clips := make([]map[string]interface{}, 0, len(s.authorClips))
	for _, clip := range s.authorClips {
		clips = append(clips, map[string]interface{}{
			"path":         clip.Path,
			"displayName":  clip.DisplayName,
			"duration":     clip.Duration,
			"chapterTitle": clip.ChapterTitle,
		})
	}
	chapters := make([]map[string]interface{}, 0, len(s.authorChapters))
	for _, ch := range s.authorChapters {
		chapters = append(chapters, map[string]interface{}{
			"timestamp": ch.Timestamp,
			"title":     ch.Title,
			"auto":      ch.Auto,
		})
	}

	config := map[string]interface{}{
		"paths":               paths,
		"region":              region,
		"aspect":              aspect,
		"title":               title,
		"outputPath":          outputPath,
		"makeISO":             makeISO,
		"treatAsChapters":     s.authorTreatAsChapters,
		"clips":               clips,
		"chapters":            chapters,
		"discSize":            s.authorDiscSize,
		"outputType":          s.authorOutputType,
		"authorTitle":         s.authorTitle,
		"authorRegion":        s.authorRegion,
		"authorAspect":        s.authorAspectRatio,
		"createMenu":          s.authorCreateMenu,
		"chapterSource":       s.authorChapterSource,
		"subtitleTracks":      append([]string{}, s.authorSubtitles...),
		"additionalAudios":    append([]string{}, s.authorAudioTracks...),
		"menuTemplate":        s.authorMenuTemplate,
		"menuBackgroundImage": s.authorMenuBackgroundImage,
		"menuTheme":           s.authorMenuTheme,
		"menuLogoEnabled":     s.authorMenuLogoEnabled,
		"menuLogoPath":        s.authorMenuLogoPath,
		"menuLogoPosition":    s.authorMenuLogoPosition,
		"menuLogoScale":       s.authorMenuLogoScale,
		"menuLogoMargin":      s.authorMenuLogoMargin,
		"menuStructure":       s.authorMenuStructure,
		"menuExtrasEnabled":   s.authorMenuExtrasEnabled,
		"menuChapterThumbSrc": s.authorMenuChapterThumbSrc,
	}

	titleLabel := title
	if strings.TrimSpace(titleLabel) == "" {
		titleLabel = "DVD"
	}

	// Sanitize output path to ensure no special characters in filesystem operations
	sanitizedOutputPath := outputPath
	dir := filepath.Dir(outputPath)
	base := filepath.Base(outputPath)
	ext := filepath.Ext(base)
	nameWithoutExt := strings.TrimSuffix(base, ext)
	sanitizedName := sanitizeForPath(nameWithoutExt)
	if sanitizedName == "" {
		sanitizedName = "dvd_output"
	}
	sanitizedOutputPath = filepath.Join(dir, sanitizedName+ext)

	job := &queue.Job{
		Type:        queue.JobTypeAuthor,
		Title:       fmt.Sprintf("Author DVD: %s", titleLabel),
		Description: fmt.Sprintf("Output: %s", utils.ShortenMiddle(filepath.Base(sanitizedOutputPath), 40)),
		InputFile:   paths[0],
		OutputFile:  sanitizedOutputPath,
		Config:      config,
	}

	s.resetAuthorLog()
	s.setAuthorStatus("Queued authoring job...")
	s.setAuthorProgress(0)
	s.jobQueue.Add(job)
	if startNow && !s.jobQueue.IsRunning() {
		s.jobQueue.Start()
	}
	// Navigate to queue view when starting a job immediately
	if startNow {
		s.showQueue()
	}
	return nil
}

func (s *appState) addAuthorVideoTSToQueue(videoTSPath, title, outputPath string, startNow bool) error {
	if s.jobQueue == nil {
		return fmt.Errorf("queue not initialized")
	}

	// Sanitize output path to ensure no special characters in filesystem operations
	sanitizedOutputPath := outputPath
	dir := filepath.Dir(outputPath)
	base := filepath.Base(outputPath)
	ext := filepath.Ext(base)
	nameWithoutExt := strings.TrimSuffix(base, ext)
	sanitizedName := sanitizeForPath(nameWithoutExt)
	if sanitizedName == "" {
		sanitizedName = "dvd_output"
	}
	sanitizedOutputPath = filepath.Join(dir, sanitizedName+ext)

	job := &queue.Job{
		Type:        queue.JobTypeAuthor,
		Title:       fmt.Sprintf("Author ISO: %s", title),
		Description: fmt.Sprintf("VIDEO_TS -> %s", utils.ShortenMiddle(filepath.Base(sanitizedOutputPath), 40)),
		InputFile:   videoTSPath,
		OutputFile:  sanitizedOutputPath,
		Config: map[string]interface{}{
			"videoTSPath": videoTSPath,
			"outputPath":  sanitizedOutputPath,
			"makeISO":     true,
			"title":       title,
		},
	}

	s.resetAuthorLog()
	s.setAuthorStatus("Queued authoring job...")
	s.setAuthorProgress(0)
	s.jobQueue.Add(job)
	if startNow && !s.jobQueue.IsRunning() {
		s.jobQueue.Start()
	}
	// Navigate to queue view when starting a job immediately
	if startNow {
		s.showQueue()
	}
	return nil
}

func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, region, aspect, title, outputPath string, makeISO bool, clips []authorClip, chapters []authorChapter, treatAsChapters bool, createMenu bool, menuTemplate string, menuBackgroundImage string, menuTheme string, menuLogoEnabled bool, menuLogoPath, menuLogoPosition string, menuLogoScale float64, menuLogoMargin int, logFn func(string), progressFn func(float64)) error {
	tempRoot := authorTempRoot(outputPath)
	if err := os.MkdirAll(tempRoot, 0755); err != nil {
		return fmt.Errorf("failed to create temp root: %w", err)
	}
	workDir, err := os.MkdirTemp(tempRoot, "videotools-author-")
	if err != nil {
		return fmt.Errorf("failed to create temp directory: %w", err)
	}
	defer os.RemoveAll(workDir)
	if logFn != nil {
		logFn(fmt.Sprintf("Temp workspace: %s", workDir))
	}

	discRoot := outputPath
	var cleanup func()
	if makeISO {
		tempRoot, err := os.MkdirTemp(tempRoot, "videotools-dvd-")
		if err != nil {
			return fmt.Errorf("failed to create DVD output directory: %w", err)
		}
		discRoot = tempRoot
		cleanup = func() {
			_ = os.RemoveAll(tempRoot)
		}
	}
	if cleanup != nil {
		defer cleanup()
	}

	if err := prepareDiscRoot(discRoot); err != nil {
		return err
	}

	var totalDuration float64
	for _, path := range paths {
		src, err := probeVideo(path)
		if err == nil {
			totalDuration += src.Duration
		}
	}

	encodingProgressShare := 80.0
	otherStepsProgressShare := 20.0
	otherStepsCount := 2.0
	if makeISO {
		otherStepsCount++
	}
	progressForOtherStep := otherStepsProgressShare / otherStepsCount
	var accumulatedProgress float64

	var mpgPaths []string
	for i, path := range paths {
		if logFn != nil {
			logFn(fmt.Sprintf("Encoding %d/%d: %s", i+1, len(paths), filepath.Base(path)))
		}
		outPath := filepath.Join(workDir, fmt.Sprintf("title_%02d.mpg", i+1))
		src, err := probeVideo(path)
		if err != nil {
			return fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
		}

		clipProgressShare := 0.0
		if totalDuration > 0 {
			clipProgressShare = (src.Duration / totalDuration) * encodingProgressShare
		}

		ffmpegProgressFn := func(stepPct float64) {
			overallPct := accumulatedProgress + (stepPct / 100.0 * clipProgressShare)
			if progressFn != nil {
				progressFn(overallPct)
			}
		}

		args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
		if logFn != nil {
			logFn(fmt.Sprintf(">> ffmpeg %s", strings.Join(args, " ")))
		}

		if err := runAuthorFFmpeg(ctx, args, src.Duration, logFn, ffmpegProgressFn); err != nil {
			return err
		}

		accumulatedProgress += clipProgressShare
		if progressFn != nil {
			progressFn(accumulatedProgress)
		}

		remuxPath := filepath.Join(workDir, fmt.Sprintf("title_%02d_remux.mpg", i+1))
		remuxArgs := []string{
			"-fflags", "+genpts",
			"-i", outPath,
			"-c", "copy",
			"-f", "dvd",
			"-muxrate", "10080000",
			"-packetsize", "2048",
			"-y", remuxPath,
		}
		if logFn != nil {
			logFn(fmt.Sprintf(">> ffmpeg %s (remuxing for DVD compliance)", strings.Join(remuxArgs, " ")))
		}
		if err := runCommandWithLogger(ctx, utils.GetFFmpegPath(), remuxArgs, logFn); err != nil {
			return fmt.Errorf("remux failed: %w", err)
		}
		os.Remove(outPath)
		mpgPaths = append(mpgPaths, remuxPath)
	}

	// Generate clips from paths if clips is empty (fallback for when job didn't save clips)
	if len(clips) == 0 && len(paths) > 1 {
		for _, path := range paths {
			src, err := probeVideo(path)
			duration := 0.0
			displayName := filepath.Base(path)
			chapterTitle := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
			if err == nil {
				duration = src.Duration
				displayName = src.DisplayName
				// Use metadata title if available, otherwise use filename
				if src.Metadata != nil {
					if title, ok := src.Metadata["title"]; ok && strings.TrimSpace(title) != "" {
						chapterTitle = title
					}
				}
			}
			clips = append(clips, authorClip{
				Path:         path,
				DisplayName:  displayName,
				Duration:     duration,
				ChapterTitle: chapterTitle,
			})
		}
		if logFn != nil {
			logFn(fmt.Sprintf("Generated %d clips from input paths for chapter markers", len(clips)))
		}
	}

	// Generate chapters from clips if available (for professional DVD navigation)
	if len(chapters) == 0 && len(clips) > 1 {
		chapters = chaptersFromClips(clips)
		if logFn != nil {
			logFn(fmt.Sprintf("Generated %d chapter markers from video clips", len(chapters)))
		}
	}

	// Try to extract embedded chapters from single file
	if len(chapters) == 0 && len(mpgPaths) == 1 {
		if embed, err := extractChaptersFromFile(paths[0]); err == nil && len(embed) > 0 {
			chapters = embed
			if logFn != nil {
				logFn(fmt.Sprintf("Extracted %d embedded chapters from source", len(chapters)))
			}
		}
	}

	// For professional DVD: always concatenate multiple files into one title with chapters
	if len(mpgPaths) > 1 {
		concatPath := filepath.Join(workDir, "titles_joined.mpg")
		if logFn != nil {
			logFn(fmt.Sprintf("Combining %d videos into single title with chapter markers...", len(mpgPaths)))
		}
		if err := concatDVDMpg(mpgPaths, concatPath); err != nil {
			return fmt.Errorf("failed to concatenate videos: %w", err)
		}
		mpgPaths = []string{concatPath}
	}

	// Log details about encoded MPG files
	if logFn != nil {
		logFn(fmt.Sprintf("Created %d MPEG file(s):", len(mpgPaths)))
		for i, mpg := range mpgPaths {
			if info, err := os.Stat(mpg); err == nil {
				logFn(fmt.Sprintf("  %d. %s (%d bytes)", i+1, filepath.Base(mpg), info.Size()))
			} else {
				logFn(fmt.Sprintf("  %d. %s (stat failed: %v)", i+1, filepath.Base(mpg), err))
			}
		}
	}

	var menuMpg string
	var menuButtons []dvdMenuButton
	if createMenu {
		template, ok := menuTemplates[menuTemplate]
		if !ok {
			template = &SimpleMenu{}
		}
		menuMpg, menuButtons, err = buildDVDMenuAssets(
			ctx,
			workDir,
			title,
			region,
			aspect,
			chapters,
			logFn,
			template,
			menuBackgroundImage,
			&MenuTheme{Name: menuTheme},
			menuLogoOptions{
				Enabled:  menuLogoEnabled,
				Path:     menuLogoPath,
				Position: menuLogoPosition,
				Scale:    menuLogoScale,
				Margin:   menuLogoMargin,
			},
		)
		if err != nil {
			return err
		}
	}

	xmlPath := filepath.Join(workDir, "dvd.xml")
	if err := writeDVDAuthorXML(xmlPath, mpgPaths, region, aspect, chapters, menuMpg, menuButtons); err != nil {
		return err
	}

	// Log chapter information
	if len(chapters) > 0 {
		if logFn != nil {
			logFn(fmt.Sprintf("Final DVD structure: 1 title with %d chapters", len(chapters)))
			for i, ch := range chapters {
				logFn(fmt.Sprintf("  Chapter %d: %s at %s", i+1, ch.Title, formatChapterTime(ch.Timestamp)))
			}
		}
	}

	// Log the XML content for debugging
	if xmlContent, err := os.ReadFile(xmlPath); err == nil {
		logFn("Generated DVD XML:")
		logFn(string(xmlContent))
	}

	logFn("Authoring DVD structure...")
	logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
	if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-x", xmlPath}, logFn); err != nil {
		logFn(fmt.Sprintf("ERROR: dvdauthor failed: %v", err))
		return fmt.Errorf("dvdauthor structure creation failed: %w", err)
	}
	accumulatedProgress += progressForOtherStep
	progressFn(accumulatedProgress)

	logFn("Building DVD tables...")
	logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
	if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-T"}, logFn); err != nil {
		logFn(fmt.Sprintf("ERROR: dvdauthor -T failed: %v", err))
		return fmt.Errorf("dvdauthor table build failed: %w", err)
	}
	accumulatedProgress += progressForOtherStep
	progressFn(accumulatedProgress)

	if err := os.MkdirAll(filepath.Join(discRoot, "AUDIO_TS"), 0755); err != nil {
		return fmt.Errorf("failed to create AUDIO_TS: %w", err)
	}

	if makeISO {
		// Create output directory for ISO file if it doesn't exist
		if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
			return fmt.Errorf("failed to create ISO output directory: %w", err)
		}
		tool, args, err := buildISOCommand(outputPath, discRoot, title)
		if err != nil {
			logFn(fmt.Sprintf("ERROR: ISO tool not found: %v", err))
			return fmt.Errorf("ISO creation setup failed: %w", err)
		}
		logFn("Creating ISO image...")
		logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
		if err := runCommandWithLogger(ctx, tool, args, logFn); err != nil {
			logFn(fmt.Sprintf("ERROR: ISO creation failed: %v", err))
			return fmt.Errorf("ISO creation failed: %w", err)
		}
		accumulatedProgress += progressForOtherStep
		progressFn(accumulatedProgress)

		// Verify ISO was created
		if info, err := os.Stat(outputPath); err == nil {
			logFn(fmt.Sprintf("ISO created successfully: %s (%d bytes)", filepath.Base(outputPath), info.Size()))
		} else {
			logFn(fmt.Sprintf("WARNING: ISO file verification failed: %v", err))
		}
	}

	progressFn(100.0)
	return nil
}

func runAuthorFFmpeg(ctx context.Context, args []string, duration float64, logFn func(string), progressFn func(float64)) error {
	finalArgs := append([]string{"-progress", "pipe:1", "-nostats"}, args...)
	cmd := exec.CommandContext(ctx, utils.GetFFmpegPath(), finalArgs...)
	utils.ApplyNoWindow(cmd)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("ffmpeg stdout pipe: %w", err)
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return fmt.Errorf("ffmpeg stderr pipe: %w", err)
	}
	if err := cmd.Start(); err != nil {
		return fmt.Errorf("ffmpeg start failed: %w", err)
	}
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		scanner := bufio.NewScanner(stderr)
		for scanner.Scan() {
			if logFn != nil {
				logFn(scanner.Text())
			}
		}
	}()
	go func() {
		defer wg.Done()
		scanner := bufio.NewScanner(stdout)
		for scanner.Scan() {
			line := scanner.Text()
			parts := strings.SplitN(line, "=", 2)
			if len(parts) < 2 {
				continue
			}
			key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
			if key == "out_time_ms" {
				if ms, err := strconv.ParseInt(val, 10, 64); err == nil && ms > 0 {
					currentSec := float64(ms) / 1000000.0
					if duration > 0 {
						stepPct := (currentSec / duration) * 100.0
						if stepPct > 100 {
							stepPct = 100
						}
						if progressFn != nil {
							progressFn(stepPct)
						}
					}
				}
			}
			if logFn != nil {
				logFn(line)
			}
		}
	}()
	err = cmd.Wait()
	wg.Wait()
	if err != nil {
		return fmt.Errorf("ffmpeg failed: %w", err)
	}
	return nil
}

func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
	cfg := job.Config
	if cfg == nil {
		return fmt.Errorf("author job config missing")
	}
	if videoTSPath := strings.TrimSpace(toString(cfg["videoTSPath"])); videoTSPath != "" {
		outputPath := toString(cfg["outputPath"])
		title := toString(cfg["title"])
		if err := ensureAuthorDependencies(true, false); err != nil {
			return err
		}

		logFile, logPath, logErr := createAuthorLog([]string{videoTSPath}, outputPath, true, "", "", title)
		if logErr != nil {
			logging.Debug(logging.CatSystem, "author log open failed: %v", logErr)
		} else {
			job.LogPath = logPath
			s.authorLogFilePath = logPath // Store for UI access
			defer logFile.Close()
		}

		appendLog := func(line string) {
			if logFile != nil {
				fmt.Fprintln(logFile, line)
			}
			app := fyne.CurrentApp()
			if app != nil && app.Driver() != nil {
				app.Driver().DoFromGoroutine(func() {
					s.appendAuthorLog(line)
				}, false)
			}
		}

		updateProgress := func(percent float64) {
			progressCallback(percent)
			app := fyne.CurrentApp()
			if app != nil && app.Driver() != nil {
				app.Driver().DoFromGoroutine(func() {
					s.setAuthorProgress(percent)
				}, false)
			}
		}

		appendLog(fmt.Sprintf("Authoring ISO from VIDEO_TS: %s", videoTSPath))
		// Create output directory for ISO file if it doesn't exist
		if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
			return fmt.Errorf("failed to create ISO output directory: %w", err)
		}
		tool, args, err := buildISOCommand(outputPath, videoTSPath, title)
		if err != nil {
			return err
		}
		appendLog(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
		updateProgress(10)
		if err := runCommandWithLogger(ctx, tool, args, appendLog); err != nil {
			return err
		}
		updateProgress(100)
		appendLog("ISO creation completed successfully.")
		return nil
	}

	rawPaths, _ := cfg["paths"].([]interface{})
	var paths []string
	for _, p := range rawPaths {
		paths = append(paths, toString(p))
	}
	if len(paths) == 0 {
		if path, ok := cfg["paths"].([]string); ok {
			paths = append(paths, path...)
		}
	}
	if len(paths) == 0 {
		if input, ok := cfg["inputPath"].(string); ok && input != "" {
			paths = append(paths, input)
		}
	}
	if len(paths) == 0 {
		return fmt.Errorf("no input paths for author job")
	}

	region := toString(cfg["region"])
	aspect := toString(cfg["aspect"])
	title := toString(cfg["title"])
	outputPath := toString(cfg["outputPath"])
	makeISO, _ := cfg["makeISO"].(bool)
	treatAsChapters, _ := cfg["treatAsChapters"].(bool)
	createMenu := toBool(cfg["createMenu"])

	if err := ensureAuthorDependencies(makeISO, createMenu); err != nil {
		return err
	}

	var clips []authorClip
	if rawClips, ok := cfg["clips"].([]interface{}); ok {
		for _, rc := range rawClips {
			if m, ok := rc.(map[string]interface{}); ok {
				clips = append(clips, authorClip{
					Path:         toString(m["path"]),
					DisplayName:  toString(m["displayName"]),
					Duration:     toFloat(m["duration"]),
					ChapterTitle: toString(m["chapterTitle"]),
				})
			}
		}
	}

	var chapters []authorChapter
	if rawChapters, ok := cfg["chapters"].([]interface{}); ok {
		for _, rc := range rawChapters {
			if m, ok := rc.(map[string]interface{}); ok {
				chapters = append(chapters, authorChapter{
					Timestamp: toFloat(m["timestamp"]),
					Title:     toString(m["title"]),
					Auto:      toBool(m["auto"]),
				})
			}
		}
	}

	logFile, logPath, logErr := createAuthorLog(paths, outputPath, makeISO, region, aspect, title)
	if logErr != nil {
		logging.Debug(logging.CatSystem, "author log open failed: %v", logErr)
	} else {
		job.LogPath = logPath
		s.authorLogFilePath = logPath // Store for UI access
		defer logFile.Close()
	}

	appendLog := func(line string) {
		if logFile != nil {
			fmt.Fprintln(logFile, line)
		}
		app := fyne.CurrentApp()
		if app != nil && app.Driver() != nil {
			app.Driver().DoFromGoroutine(func() {
				s.appendAuthorLog(line)
			}, false)
		}
	}

	updateProgress := func(percent float64) {
		progressCallback(percent)
		app := fyne.CurrentApp()
		if app != nil && app.Driver() != nil {
			app.Driver().DoFromGoroutine(func() {
				s.setAuthorProgress(percent)
			}, false)
		}
	}

	appendLog(fmt.Sprintf("Authoring started: %s", time.Now().Format(time.RFC3339)))
	appendLog(fmt.Sprintf("Inputs: %s", strings.Join(paths, ", ")))
	appendLog(fmt.Sprintf("Output: %s", outputPath))
	if makeISO {
		appendLog("Output mode: ISO")
	} else {
		appendLog("Output mode: VIDEO_TS")
	}

	app := fyne.CurrentApp()
	if app != nil && app.Driver() != nil {
		app.Driver().DoFromGoroutine(func() {
			s.setAuthorStatus("Authoring in progress...")
		}, false)
	}

	err := s.runAuthoringPipeline(
		ctx,
		paths,
		region,
		aspect,
		title,
		outputPath,
		makeISO,
		clips,
		chapters,
		treatAsChapters,
		createMenu,
		toString(cfg["menuTemplate"]),
		toString(cfg["menuBackgroundImage"]),
		toString(cfg["menuTheme"]),
		toBool(cfg["menuLogoEnabled"]),
		toString(cfg["menuLogoPath"]),
		toString(cfg["menuLogoPosition"]),
		toFloat(cfg["menuLogoScale"]),
		int(toFloat(cfg["menuLogoMargin"])),
		appendLog,
		updateProgress,
	)
	if err != nil {
		friendly := authorFriendlyError(err)
		appendLog("ERROR: " + friendly)
		if app != nil && app.Driver() != nil {
			app.Driver().DoFromGoroutine(func() {
				s.setAuthorStatus(friendly)
			}, false)
		}
		return fmt.Errorf("%s\nSee Authoring Log for details.", friendly)
	}

	if app != nil && app.Driver() != nil {
		app.Driver().DoFromGoroutine(func() {
			s.setAuthorStatus("Authoring complete")
			s.setAuthorProgress(100)
		}, false)
	}
	appendLog("Authoring completed successfully.")
	return nil
}

func authorFriendlyError(err error) string {
	if err == nil {
		return "Authoring failed"
	}
	msg := err.Error()
	lower := strings.ToLower(msg)
	switch {
	case strings.Contains(lower, "disk quota exceeded"),
		strings.Contains(lower, "no space left"),
		strings.Contains(lower, "not enough space"):
		return "Not enough disk space for authoring output."
	case strings.Contains(lower, "output folder must be empty"):
		return "Output folder must be empty before authoring."
	case strings.Contains(lower, "dvdauthor not found"):
		return "dvdauthor not found. Install DVD authoring tools."
	case strings.Contains(lower, "mkisofs"),
		strings.Contains(lower, "genisoimage"),
		strings.Contains(lower, "xorriso"):
		return "ISO tool not found. Install mkisofs/genisoimage/xorriso."
	case strings.Contains(lower, "permission denied"):
		return "Permission denied writing to output folder."
	case strings.Contains(lower, "ffmpeg"):
		return "FFmpeg failed during DVD encoding."
	default:
		if len(msg) > 140 {
			return "Authoring failed. See Authoring Log for details."
		}
		return msg
	}
}

func prepareDiscRoot(path string) error {
	if err := os.MkdirAll(path, 0755); err != nil {
		return fmt.Errorf("failed to create output directory: %w", err)
	}

	entries, err := os.ReadDir(path)
	if err != nil {
		return fmt.Errorf("failed to read output directory: %w", err)
	}
	if len(entries) > 0 {
		return fmt.Errorf("output folder must be empty: %s", path)
	}
	return nil
}

func encodeAuthorSources(paths []string, region, aspect, workDir string) ([]string, error) {
	var mpgPaths []string
	for i, path := range paths {
		idx := i + 1
		outPath := filepath.Join(workDir, fmt.Sprintf("title_%02d.mpg", idx))
		src, err := probeVideo(path)
		if err != nil {
			return nil, fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
		}
		args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
		if err := runCommand(utils.GetFFmpegPath(), args); err != nil {
			return nil, err
		}
		mpgPaths = append(mpgPaths, outPath)
	}
	return mpgPaths, nil
}

func buildAuthorFFmpegArgs(inputPath, outputPath, region, aspect string, progressive bool) []string {
	width := 720
	height := 480
	fps := "30000/1001"
	gop := "15"
	bitrate := "6000k"
	maxrate := "9000k"

	if region == "PAL" {
		height = 576
		fps = "25"
		gop = "12"
		bitrate = "8000k"
		maxrate = "9500k"
	}

	vf := []string{
		fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", width, height),
		fmt.Sprintf("pad=%d:%d:(ow-iw)/2:(oh-ih)/2", width, height),
		fmt.Sprintf("setdar=%s", aspect),
		"setsar=1",
		fmt.Sprintf("fps=%s", fps),
	}

	args := []string{
		"-y",
		"-hide_banner",
		"-loglevel", "error",
		"-i", inputPath,
		"-vf", strings.Join(vf, ","),
		"-c:v", "mpeg2video",
		"-r", fps,
		"-b:v", bitrate,
		"-maxrate", maxrate,
		"-bufsize", "1835k",
		"-g", gop,
		"-pix_fmt", "yuv420p",
	}

	if !progressive {
		args = append(args, "-flags", "+ilme+ildct")
	}

	args = append(args,
		"-c:a", "ac3",
		"-b:a", "192k",
		"-ar", "48000",
		"-ac", "2",
		"-f", "dvd", // DVD-compliant MPEG-PS format
		"-muxrate", "10080000", // DVD mux rate (10.08 Mbps)
		"-packetsize", "2048", // DVD packet size
		outputPath,
	)

	return args
}

func writeDVDAuthorXML(path string, mpgPaths []string, region, aspect string, chapters []authorChapter, menuMpg string, menuButtons []dvdMenuButton) error {
	format := strings.ToLower(region)
	if format != "pal" {
		format = "ntsc"
	}

	var b strings.Builder
	b.WriteString("<dvdauthor>\n")
	if menuMpg != "" && len(menuButtons) > 0 {
		b.WriteString("  <vmgm>\n")
		b.WriteString("    <menus>\n")
		b.WriteString("      <pgc entry=\"root\">\n")
		b.WriteString(fmt.Sprintf("        <vob file=\"%s\" pause=\"inf\" />\n", escapeXMLAttr(menuMpg)))
		for _, btn := range menuButtons {
			b.WriteString(fmt.Sprintf("        <button>%s</button>\n", btn.Command))
		}
		b.WriteString("      </pgc>\n")
		b.WriteString("    </menus>\n")
		b.WriteString("  </vmgm>\n")
	} else {
		b.WriteString("  <vmgm />\n")
	}
	b.WriteString("  <titleset>\n")
	b.WriteString("    <titles>\n")
	b.WriteString(fmt.Sprintf("      <video format=\"%s\" aspect=\"%s\" />\n", format, aspect))
	for _, mpg := range mpgPaths {
		b.WriteString("      <pgc>\n")
		if len(chapters) > 0 {
			b.WriteString(fmt.Sprintf("        <vob file=\"%s\" chapters=\"%s\" />\n", escapeXMLAttr(mpg), chaptersToDVDAuthor(chapters)))
		} else {
			b.WriteString(fmt.Sprintf("        <vob file=\"%s\" />\n", escapeXMLAttr(mpg)))
		}
		b.WriteString("      </pgc>\n")
	}
	b.WriteString("    </titles>\n")
	b.WriteString("  </titleset>\n")
	b.WriteString("</dvdauthor>\n")

	if err := os.WriteFile(path, []byte(b.String()), 0644); err != nil {
		return fmt.Errorf("failed to write dvdauthor XML: %w", err)
	}
	return nil
}

func escapeXMLAttr(value string) string {
	var b strings.Builder
	if err := xml.EscapeText(&b, []byte(value)); err != nil {
		return strings.ReplaceAll(value, "\"", "&quot;")
	}
	escaped := b.String()
	return strings.ReplaceAll(escaped, "\"", "&quot;")
}

func ensureAuthorDependencies(makeISO bool, createMenu bool) error {
	if err := ensureExecutable(utils.GetFFmpegPath(), "ffmpeg"); err != nil {
		return err
	}
	if _, err := exec.LookPath("dvdauthor"); err != nil {
		return fmt.Errorf("dvdauthor not found in PATH")
	}
	if createMenu {
		if _, err := exec.LookPath("spumux"); err != nil {
			return fmt.Errorf("spumux not found in PATH")
		}
	}
	if makeISO {
		if _, _, err := buildISOCommand("output.iso", "output", "VIDEO_TOOLS"); err != nil {
			return err
		}
	}
	return nil
}

func createAuthorLog(inputs []string, outputPath string, makeISO bool, region, aspect, title string) (*os.File, string, error) {
	base := strings.TrimSuffix(filepath.Base(outputPath), filepath.Ext(outputPath))
	if base == "" {
		base = "author"
	}
	// Sanitize log filename to remove special characters
	base = sanitizeForPath(base)
	if base == "" {
		base = "author"
	}
	// Add timestamp prefix for chronological sorting and uniqueness
	timestamp := time.Now().Format("20060102_150405")
	logFilename := fmt.Sprintf("%s-%s-author%s", timestamp, base, conversionLogSuffix)
	logPath := filepath.Join(getLogsDir(), logFilename)
	if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
		return nil, logPath, fmt.Errorf("create log dir: %w", err)
	}
	f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
	if err != nil {
		return nil, logPath, err
	}
	mode := "VIDEO_TS"
	if makeISO {
		mode = "ISO"
	}
	header := fmt.Sprintf(`VideoTools Authoring Log
Started: %s
Inputs: %s
Output: %s
Mode: %s
Region: %s
Aspect: %s
Title: %s

`, time.Now().Format(time.RFC3339), strings.Join(inputs, ", "), outputPath, mode, region, aspect, title)
	if _, err := f.WriteString(header); err != nil {
		_ = f.Close()
		return nil, logPath, err
	}
	return f, logPath, nil
}

func runCommandWithLogger(ctx context.Context, name string, args []string, logFn func(string)) error {
	cmd := exec.CommandContext(ctx, name, args...)
	utils.ApplyNoWindow(cmd)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("%s stdout: %w", name, err)
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return fmt.Errorf("%s stderr: %w", name, err)
	}

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("%s start: %w", name, err)
	}

	var wg sync.WaitGroup
	stream := func(r io.Reader) {
		defer wg.Done()
		scanner := bufio.NewScanner(r)
		scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
		for scanner.Scan() {
			if logFn != nil {
				logFn(scanner.Text())
			}
		}
	}
	wg.Add(2)
	go stream(stdout)
	go stream(stderr)

	err = cmd.Wait()
	wg.Wait()
	if err != nil {
		return fmt.Errorf("%s failed: %w", name, err)
	}
	return nil
}

func toBool(v interface{}) bool {
	switch val := v.(type) {
	case bool:
		return val
	case string:
		return strings.EqualFold(val, "true")
	case float64:
		return val != 0
	case int:
		return val != 0
	default:
		return false
	}
}

func ensureExecutable(path, label string) error {
	if filepath.IsAbs(path) {
		if _, err := os.Stat(path); err == nil {
			return nil
		}
	}
	if _, err := exec.LookPath(path); err == nil {
		return nil
	}
	return fmt.Errorf("%s not found (%s)", label, path)
}

func buildISOCommand(outputISO, discRoot, title string) (string, []string, error) {
	tool, prefixArgs, err := findISOTool()
	if err != nil {
		return "", nil, err
	}
	label := isoVolumeLabel(title)
	args := append([]string{}, prefixArgs...)
	args = append(args, "-dvd-video", "-V", label, "-o", outputISO, discRoot)
	return tool, args, nil
}

func findISOTool() (string, []string, error) {
	if path, err := exec.LookPath("mkisofs"); err == nil {
		return path, nil, nil
	}
	if path, err := exec.LookPath("genisoimage"); err == nil {
		return path, nil, nil
	}
	if path, err := exec.LookPath("xorriso"); err == nil {
		return path, []string{"-as", "mkisofs"}, nil
	}
	return "", nil, fmt.Errorf("mkisofs, genisoimage, or xorriso not found in PATH")
}

func isoVolumeLabel(title string) string {
	label := strings.ToUpper(strings.TrimSpace(title))
	if label == "" {
		label = "VIDEO_TOOLS"
	}
	var b strings.Builder
	for _, r := range label {
		switch {
		case r >= 'A' && r <= 'Z':
			b.WriteRune(r)
		case r >= '0' && r <= '9':
			b.WriteRune(r)
		case r == '_' || r == '-':
			b.WriteRune('_')
		default:
			b.WriteRune('_')
		}
	}
	clean := strings.Trim(b.String(), "_")
	if clean == "" {
		clean = "VIDEO_TOOLS"
	}
	if len(clean) > 32 {
		clean = clean[:32]
	}
	return clean
}

func runCommand(name string, args []string) error {
	cmd := exec.Command(name, args...)
	utils.ApplyNoWindow(cmd)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("%s failed: %s", name, strings.TrimSpace(string(output)))
	}
	return nil
}

func (s *appState) showChapterPreview(videoPath string, chapters []authorChapter, callback func(bool)) {
	dlg := dialog.NewCustom("Chapter Preview", "Close", container.NewVBox(
		widget.NewLabel(fmt.Sprintf("Detected %d chapters - generating thumbnails...", len(chapters))),
		widget.NewProgressBarInfinite(),
	), s.window)
	dlg.Resize(fyne.NewSize(800, 600))
	dlg.Show()

	go func() {
		// Limit preview to first 24 chapters for performance
		previewCount := len(chapters)
		if previewCount > 24 {
			previewCount = 24
		}

		thumbnails := make([]fyne.CanvasObject, 0, previewCount)
		for i := 0; i < previewCount; i++ {
			ch := chapters[i]
			thumbPath, err := extractChapterThumbnail(videoPath, ch.Timestamp)
			if err != nil {
				logging.Debug(logging.CatSystem, "failed to extract thumbnail at %.2f: %v", ch.Timestamp, err)
				continue
			}

			img := canvas.NewImageFromFile(thumbPath)
			img.FillMode = canvas.ImageFillContain
			img.SetMinSize(fyne.NewSize(160, 90))

			timeLabel := widget.NewLabel(fmt.Sprintf("%.2fs", ch.Timestamp))
			timeLabel.Alignment = fyne.TextAlignCenter

			thumbCard := container.NewVBox(
				container.NewMax(img),
				timeLabel,
			)
			thumbnails = append(thumbnails, thumbCard)
		}

		runOnUI(func() {
			dlg.Hide()

			if len(thumbnails) == 0 {
				dialog.ShowError(fmt.Errorf("failed to generate chapter thumbnails"), s.window)
				return
			}

			grid := container.NewGridWrap(fyne.NewSize(170, 120), thumbnails...)
			scroll := container.NewVScroll(grid)
			// scroll.SetMinSize(fyne.NewSize(780, 500)) // Removed for flexible sizing

			infoText := fmt.Sprintf("Found %d chapters", len(chapters))
			if len(chapters) > previewCount {
				infoText += fmt.Sprintf(" (showing first %d)", previewCount)
			}
			info := widget.NewLabel(infoText)
			info.Wrapping = fyne.TextWrapWord

			var previewDlg *dialog.CustomDialog
			acceptBtn := widget.NewButton("Accept Chapters", func() {
				previewDlg.Hide()
				callback(true)
			})
			acceptBtn.Importance = widget.HighImportance

			rejectBtn := widget.NewButton("Reject", func() {
				previewDlg.Hide()
				callback(false)
			})

			content := container.NewBorder(
				container.NewVBox(info, widget.NewSeparator()),
				container.NewHBox(rejectBtn, acceptBtn),
				nil,
				nil,
				scroll,
			)

			previewDlg = dialog.NewCustom("Chapter Preview", "Close", content, s.window)
			previewDlg.Resize(fyne.NewSize(800, 600))
			previewDlg.Show()
		})
	}()
}

func extractChapterThumbnail(videoPath string, timestamp float64) (string, error) {
	tmpDir := filepath.Join(os.TempDir(), "videotools-chapter-thumbs")
	if err := os.MkdirAll(tmpDir, 0755); err != nil {
		return "", err
	}

	outputPath := filepath.Join(tmpDir, fmt.Sprintf("thumb_%.2f.jpg", timestamp))
	args := []string{
		"-ss", fmt.Sprintf("%.2f", timestamp),
		"-i", videoPath,
		"-frames:v", "1",
		"-q:v", "2",
		"-vf", "scale=320:180",
		"-y",
		outputPath,
	}

	cmd := exec.Command(utils.GetFFmpegPath(), args...)
	utils.ApplyNoWindow(cmd)
	if err := cmd.Run(); err != nil {
		return "", err
	}

	return outputPath, nil
}

func runOnUI(fn func()) {
	fn()
}
