package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/grafov/m3u8"
)

var roomDossierRegexp = regexp.MustCompile(`window\.initialRoomDossier = "(.*?)"`)

// --- Gemeinsame Status-Werte für MFC ---
type Status int

const (
	StatusUnknown Status = iota
	StatusPublic
	StatusPrivate
	StatusOffline
	StatusNotExist
)

func (s Status) String() string {
	switch s {
	case StatusPublic:
		return "PUBLIC"
	case StatusPrivate:
		return "PRIVATE"
	case StatusOffline:
		return "OFFLINE"
	case StatusNotExist:
		return "NOTEXIST"
	default:
		return "UNKNOWN"
	}
}

// HTTPClient kapselt http.Client + Header/Cookies (wie internal.Req im DVR)
type HTTPClient struct {
	client    *http.Client
	userAgent string
}

// gemeinsamen HTTP-Client erzeugen
func NewHTTPClient(userAgent string) *HTTPClient {
	if userAgent == "" {
		// Default, falls kein UA übergeben wird
		userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
	}

	return &HTTPClient{
		client: &http.Client{
			Timeout: 10 * time.Second,
		},
		userAgent: userAgent,
	}
}

// Request-Erstellung mit User-Agent + Cookies
func (h *HTTPClient) NewRequest(ctx context.Context, method, url, cookieStr string) (*http.Request, error) {
	req, err := http.NewRequestWithContext(ctx, method, url, nil)
	if err != nil {
		return nil, err
	}

	// Basis-Header, die immer gesetzt werden
	if h.userAgent != "" {
		req.Header.Set("User-Agent", h.userAgent)
	} else {
		req.Header.Set("User-Agent", "Mozilla/5.0")
	}
	req.Header.Set("Accept", "*/*")

	// Cookie-String wie "name=value; foo=bar"
	addCookiesFromString(req, cookieStr)

	return req, nil
}

// Seite laden + einfache Erkennung von Schutzseiten (Cloudflare / Age-Gate)
func (h *HTTPClient) FetchPage(ctx context.Context, url, cookieStr string) (string, error) {
	req, err := h.NewRequest(ctx, http.MethodGet, url, cookieStr)
	if err != nil {
		return "", err
	}

	resp, err := h.client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	body := string(data)

	// Etwas aussagekräftigere Fehler als nur "room dossier nicht gefunden"
	if strings.Contains(body, "<title>Just a moment...</title>") {
		return "", errors.New("Schutzseite von Cloudflare erhalten (\"Just a moment...\") – kein Room-HTML")
	}
	if strings.Contains(body, "Verify your age") {
		return "", errors.New("Altersverifikationsseite erhalten – kein Room-HTML")
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("HTTP %d beim Laden von %s", resp.StatusCode, url)
	}

	return body, nil
}

// --- main ---
func main() {
	if len(os.Args) < 2 {
		fmt.Println("Verwendung: recorder.exe [--http-cookie \"<Cookie>\"] [--user-agent \"<UA>\"] [--domain \"https://chaturbate.com/\"] <URL oder Benutzername> [best] [-o <Pfad/zieldatei.ts>] [-f]")
		os.Exit(1)
	}

	var (
		httpCookie string
		outputPath string
		urlArg     string
		userAgent  string
		domain     = "https://chaturbate.com/" // Standard wie beim DVR
	)

	args := os.Args[1:]

	for i := 0; i < len(args); i++ {
		a := args[i]

		switch a {
		case "--http-cookie":
			if i+1 >= len(args) {
				fmt.Println("Fehlender Wert nach --http-cookie")
				os.Exit(1)
			}
			httpCookie = args[i+1]
			i++

		case "--user-agent":
			if i+1 >= len(args) {
				fmt.Println("Fehlender Wert nach --user-agent")
				os.Exit(1)
			}
			userAgent = args[i+1]
			i++

		case "--domain":
			if i+1 >= len(args) {
				fmt.Println("Fehlender Wert nach --domain")
				os.Exit(1)
			}
			domain = args[i+1]
			i++

		case "-o":
			if i+1 >= len(args) {
				fmt.Println("Fehlender Wert nach -o")
				os.Exit(1)
			}
			outputPath = args[i+1]
			i++

		case "-f":
			// nur für Kompatibilität, wird ignoriert

		case "best":
			// wird ignoriert, wir wählen sowieso beste Qualität

		default:
			// erste „normale“ Angabe ist die URL / der Benutzername
			if urlArg == "" {
				urlArg = a
			}
		}
	}

	if urlArg == "" {
		fmt.Println("Keine URL / kein Benutzername angegeben.")
		os.Exit(1)
	}

	// Provider anhand der URL erkennen
	provider := detectProvider(urlArg)

	ctx := context.Background()
	hc := NewHTTPClient(userAgent)

	switch provider {

	case "chaturbate":
		// wie bisher
		username := extractUsername(urlArg)
		username = strings.Trim(username, "/\\") // trailing Slash entfernen

		if outputPath == "" {
			outputPath = fmt.Sprintf("%s_%s.ts", username, time.Now().Format("20060102_150405"))
		}

		if err := RecordStream(ctx, hc, domain, username, outputPath, httpCookie); err != nil {
			fmt.Println("❌ Aufnahmefehler (CB):", err)
			os.Exit(1)
		}

	case "mfc":
		if outputPath == "" {
			outputPath = fmt.Sprintf("mfc_%s.mp4", time.Now().Format("20060102_150405"))
		}
		if err := RecordStreamMFC(ctx, hc, urlArg, outputPath, httpCookie); err != nil {
			fmt.Println("❌ Aufnahmefehler (MFC):", err)
			os.Exit(1)
		}

	default:
		fmt.Println("Unbekannter oder nicht unterstützter Provider für URL:", urlArg)
		os.Exit(1)
	}

	// TS -> MP4 remuxen nur für Chaturbate
	if provider == "chaturbate" {
		mp4Out := outputPath
		ext := filepath.Ext(outputPath)
		if ext != ".mp4" {
			mp4Out = strings.TrimSuffix(outputPath, ext) + ".mp4"
		}

		if err := exec.Command(
			"ffmpeg",
			"-y",
			"-i", outputPath,
			"-c:v", "copy",
			"-c:a", "copy",
			"-bsf:a", "aac_adtstoasc",
			"-movflags", "+faststart",
			mp4Out,
		).Run(); err != nil {
			fmt.Println("⚠️ Fehler bei Umwandlung in MP4:", err)
		} else {
			fmt.Println("✅ Umwandlung abgeschlossen (web-optimiert):", mp4Out)
		}
	}
}

// --- DVR-ähnlicher Recorder-Ablauf ---
// Entspricht grob dem RecordStream aus dem Channel-Snippet:
func RecordStream(
	ctx context.Context,
	hc *HTTPClient,
	domain string,
	username string,
	outputPath string,
	httpCookie string,
) error {
	// 1) Seite laden
	// Domain sauber zusammenbauen (mit/ohne Slash)
	base := strings.TrimRight(domain, "/")
	pageURL := base + "/" + username

	body, err := hc.FetchPage(ctx, pageURL, httpCookie)

	// 2) HLS-URL aus roomDossier extrahieren (wie DVR.ParseStream)
	hlsURL, err := ParseStream(body)
	if err != nil {
		return fmt.Errorf("stream-parsing: %w", err)
	}

	// 3) Playlist holen (wie stream.GetPlaylist im DVR)
	playlist, err := FetchPlaylist(ctx, hc, hlsURL, httpCookie)
	if err != nil {
		return fmt.Errorf("playlist abrufen: %w", err)
	}
	fmt.Printf("Stream-Qualität: %dp @ %dfps\n", playlist.Resolution, playlist.Framerate)

	// 4) Datei öffnen
	file, err := os.Create(outputPath)
	if err != nil {
		return fmt.Errorf("datei erstellen: %w", err)
	}
	defer func() {
		_ = file.Close()
	}()

	fmt.Println("📡 Aufnahme gestartet:", outputPath)

	// 5) Segmente „watchen“ – analog zu WatchSegments + HandleSegment im DVR
	err = playlist.WatchSegments(ctx, hc, httpCookie, func(b []byte, duration float64) error {
		// Hier wäre im DVR ch.HandleSegment – bei dir einfach in eine Datei schreiben
		if _, err := file.Write(b); err != nil {
			return fmt.Errorf("schreibe segment: %w", err)
		}
		// Könntest hier z.B. auch Dauer/Größe loggen, wenn du möchtest
		_ = duration // aktuell unbenutzt
		return nil
	})
	if err != nil {
		return fmt.Errorf("watch segments: %w", err)
	}

	return nil
}

// RecordStreamMFC nimmt vorerst die URL 1:1 und ruft ffmpeg direkt darauf auf.
// In der Praxis musst du hier meist erst eine HLS-URL aus dem HTML extrahieren.
// RecordStreamMFC ist jetzt nur noch ein Wrapper um den bewährten MFC-Flow (runMFC).
func RecordStreamMFC(
	ctx context.Context,
	hc *HTTPClient,
	pageURL string,
	outputPath string,
	httpCookie string,
) error {
	_ = ctx        // wird im aktuellen MFC-Flow nicht benötigt
	_ = hc         // ebenfalls nicht benötigt
	_ = httpCookie // MFC-Flow nutzt aktuell keine Cookies

	username := extractMFCUsername(pageURL)
	if username == "" {
		return fmt.Errorf("konnte MFC-Username aus URL %q nicht ermitteln", pageURL)
	}

	return runMFC(username, outputPath)
}

func detectProvider(raw string) string {
	s := strings.ToLower(raw)

	if strings.Contains(s, "chaturbate.com") {
		return "chaturbate"
	}
	if strings.Contains(s, "myfreecams.com") {
		return "mfc"
	}
	return "unknown"
}

// --- helper ---
func extractUsername(input string) string {
	input = strings.TrimSpace(input)
	input = strings.TrimPrefix(input, "https://")
	input = strings.TrimPrefix(input, "http://")
	input = strings.TrimPrefix(input, "www.")
	if strings.HasPrefix(input, "chaturbate.com/") {
		input = strings.TrimPrefix(input, "chaturbate.com/")
	}

	// alles nach dem ersten Slash abschneiden (Pfadteile, /, etc.)
	if idx := strings.IndexAny(input, "/?#"); idx != -1 {
		input = input[:idx]
	}

	// zur Sicherheit evtl. übrig gebliebene Slash/Backslash trimmen
	return strings.Trim(input, "/\\")
}

// Cookie-Hilfsfunktion (wie ParseCookies + AddCookie im DVR)
func addCookiesFromString(req *http.Request, cookieStr string) {
	if cookieStr == "" {
		return
	}
	pairs := strings.Split(cookieStr, ";")
	for _, pair := range pairs {
		parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
		if len(parts) != 2 {
			continue
		}
		name := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		if name == "" {
			continue
		}
		req.AddCookie(&http.Cookie{
			Name:  name,
			Value: value,
		})
	}
}

// ParseStream entspricht der DVR-Variante (roomDossier → hls_source)
func ParseStream(html string) (string, error) {
	matches := roomDossierRegexp.FindStringSubmatch(html)
	if len(matches) == 0 {
		return "", errors.New("room dossier nicht gefunden")
	}

	// DVR-Style Unicode-Decode
	decoded, err := strconv.Unquote(
		strings.Replace(strconv.Quote(matches[1]), `\\u`, `\u`, -1),
	)
	if err != nil {
		return "", fmt.Errorf("Unicode-decode failed: %w", err)
	}

	var rd struct {
		HLSSource string `json:"hls_source"`
	}
	if err := json.Unmarshal([]byte(decoded), &rd); err != nil {
		return "", fmt.Errorf("JSON-parse failed: %w", err)
	}
	if rd.HLSSource == "" {
		return "", errors.New("kein HLS-Quell-URL im JSON")
	}
	return rd.HLSSource, nil
}

// --- Playlist/WatchSegments wie gehabt ---
type Playlist struct {
	PlaylistURL string
	RootURL     string
	Resolution  int
	Framerate   int
}

type Resolution struct {
	Framerate map[int]string
	Width     int
}

// nimmt jetzt *HTTPClient entgegen
func FetchPlaylist(ctx context.Context, hc *HTTPClient, hlsSource, httpCookie string) (*Playlist, error) {
	if hlsSource == "" {
		return nil, errors.New("HLS-URL leer")
	}

	req, err := hc.NewRequest(ctx, http.MethodGet, hlsSource, httpCookie)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Erstellen der Playlist-Request: %w", err)
	}

	resp, err := hc.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Laden der Playlist: %w", err)
	}
	defer resp.Body.Close()

	playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
	if err != nil || listType != m3u8.MASTER {
		return nil, errors.New("keine gültige Master-Playlist")
	}

	master := playlist.(*m3u8.MasterPlaylist)
	var bestURI string
	var bestWidth int
	var bestFramerate int

	for _, variant := range master.Variants {
		if variant == nil || variant.Resolution == "" {
			continue
		}

		parts := strings.Split(variant.Resolution, "x")
		if len(parts) != 2 {
			continue
		}

		width, err := strconv.Atoi(parts[1])
		if err != nil {
			continue
		}

		fr := 30
		if strings.Contains(variant.Name, "FPS:60.0") {
			fr = 60
		}

		if width > bestWidth || (width == bestWidth && fr > bestFramerate) {
			bestWidth = width
			bestFramerate = fr
			bestURI = variant.URI
		}
	}

	if bestURI == "" {
		return nil, errors.New("keine gültige Auflösung gefunden")
	}

	root := hlsSource[:strings.LastIndex(hlsSource, "/")+1]
	return &Playlist{
		PlaylistURL: root + bestURI,
		RootURL:     root,
		Resolution:  bestWidth,
		Framerate:   bestFramerate,
	}, nil
}

// nutzt ebenfalls *HTTPClient
func (p *Playlist) WatchSegments(
	ctx context.Context,
	hc *HTTPClient,
	httpCookie string,
	handler func([]byte, float64) error,
) error {
	var lastSeq int64 = -1
	emptyRounds := 0
	const maxEmptyRounds = 5

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		// Playlist holen
		req, err := hc.NewRequest(ctx, http.MethodGet, p.PlaylistURL, httpCookie)
		if err != nil {
			return fmt.Errorf("Fehler beim Erstellen der Playlist-Request: %w", err)
		}

		resp, err := hc.client.Do(req)
		if err != nil {
			emptyRounds++
			if emptyRounds >= maxEmptyRounds {
				return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline")
			}
			time.Sleep(2 * time.Second)
			continue
		}

		playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
		resp.Body.Close()

		if err != nil || listType != m3u8.MEDIA {
			emptyRounds++
			if emptyRounds >= maxEmptyRounds {
				return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
			}
			time.Sleep(2 * time.Second)
			continue
		}

		media := playlist.(*m3u8.MediaPlaylist)
		newSegment := false

		for _, segment := range media.Segments {
			if segment == nil {
				continue
			}
			if int64(segment.SeqId) <= lastSeq {
				continue
			}

			lastSeq = int64(segment.SeqId)
			newSegment = true

			segmentURL := p.RootURL + segment.URI

			segReq, err := hc.NewRequest(ctx, http.MethodGet, segmentURL, httpCookie)
			if err != nil {
				continue
			}

			segResp, err := hc.client.Do(segReq)
			if err != nil {
				continue
			}

			data, err := io.ReadAll(segResp.Body)
			segResp.Body.Close()
			if err != nil || len(data) == 0 {
				continue
			}

			if err := handler(data, segment.Duration); err != nil {
				return err
			}
		}

		if newSegment {
			emptyRounds = 0
		} else {
			emptyRounds++
			if emptyRounds >= maxEmptyRounds {
				return errors.New("🛑 Keine neuen HLS-Segmente empfangen – Stream vermutlich beendet oder offline.")
			}
		}

		time.Sleep(1 * time.Second)
	}
}

/* ───────────────────────────────
   MyFreeCams (übernommener Flow)
   ─────────────────────────────── */

type MyFreeCams struct {
	Username string
	Attrs    map[string]string
	VideoURL string
}

func NewMyFreeCams(username string) *MyFreeCams {
	return &MyFreeCams{
		Username: username,
		Attrs:    map[string]string{},
	}
}

func (m *MyFreeCams) GetWebsiteURL() string {
	return "https://www.myfreecams.com/#" + m.Username
}

func (m *MyFreeCams) GetVideoURL(refresh bool) (string, error) {
	if !refresh && m.VideoURL != "" {
		return m.VideoURL, nil
	}

	// Prüfen, ob alle benötigten Attribute vorhanden sind
	if _, ok := m.Attrs["data-cam-preview-model-id-value"]; !ok {
		return "", nil
	}
	sid := m.Attrs["data-cam-preview-server-id-value"]
	midBase := m.Attrs["data-cam-preview-model-id-value"]
	isWzobs := strings.ToLower(m.Attrs["data-cam-preview-is-wzobs-value"]) == "true"

	midInt, err := strconv.Atoi(midBase)
	if err != nil {
		return "", fmt.Errorf("model-id parse error: %w", err)
	}
	mid := 100000000 + midInt
	a := ""
	if isWzobs {
		a = "a_"
	}

	playlistURL := fmt.Sprintf(
		"https://previews.myfreecams.com/hls/NxServer/%s/ngrp:mfc_%s%d.f4v_mobile_mhp1080_previewurl/playlist.m3u8",
		sid, a, mid,
	)

	// Validieren (HTTP 200) & ggf. auf gewünschte Auflösung verlinken
	u, err := getWantedResolutionPlaylist(playlistURL)
	if err != nil {
		return "", err
	}
	m.VideoURL = u
	return m.VideoURL, nil
}

func (m *MyFreeCams) GetStatus() (Status, error) {
	// 1) share-Seite prüfen (existiert/nicht existiert)
	shareURL := "https://share.myfreecams.com/" + m.Username
	resp, err := http.Get(shareURL)
	if err != nil {
		return StatusUnknown, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return StatusNotExist, nil
	}
	if resp.StatusCode != 200 {
		return StatusUnknown, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	// wir brauchen sowohl Bytes (für Suche) als auch Reader (für HTML)
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return StatusUnknown, err
	}

	// 2) „tracking.php?“ suchen und prüfen, ob model_id vorhanden ist
	start := bytes.Index(bodyBytes, []byte("https://www.myfreecams.com/php/tracking.php?"))
	if start == -1 {
		// ohne tracking Parameter -> behandeln wie nicht existent
		return StatusNotExist, nil
	}
	end := bytes.IndexByte(bodyBytes[start:], '"')
	if end == -1 {
		return StatusUnknown, errors.New("tracking url parse failed")
	}
	raw := string(bodyBytes[start : start+end])
	u, err := url.Parse(raw)
	if err != nil {
		return StatusUnknown, fmt.Errorf("tracking url invalid: %w", err)
	}
	qs := u.Query()
	if qs.Get("model_id") == "" {
		return StatusNotExist, nil
	}

	// 3) HTML parsen und <div class="campreview" ...> Attribute auslesen
	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes))
	if err != nil {
		return StatusUnknown, err
	}

	params := doc.Find(".campreview").First()
	if params.Length() == 0 {
		// keine campreview -> offline
		return StatusOffline, nil
	}

	attrs := map[string]string{}
	params.Each(func(_ int, s *goquery.Selection) {
		for _, a := range []string{
			"data-cam-preview-server-id-value",
			"data-cam-preview-model-id-value",
			"data-cam-preview-is-wzobs-value",
		} {
			if v, ok := s.Attr(a); ok {
				attrs[a] = v
			}
		}
	})
	m.Attrs = attrs

	// 4) Versuchen, VideoURL (Preview-HLS) zu ermitteln
	uStr, err := m.GetVideoURL(true)
	if err != nil {
		return StatusUnknown, err
	}
	if uStr != "" {
		return StatusPublic, nil
	}
	// campreview vorhanden, aber keine playable url -> „PRIVATE“
	return StatusPrivate, nil
}

func runMFC(username string, outArg string) error {
	mfc := NewMyFreeCams(username)
	st, err := mfc.GetStatus()
	if err != nil {
		return err
	}
	fmt.Println("MFC Status:", st)

	if st != StatusPublic {
		return fmt.Errorf("Stream ist nicht öffentlich (Status: %s)", st)
	}
	m3u8URL, err := mfc.GetVideoURL(false)
	if err != nil {
		return err
	}
	if m3u8URL == "" {
		return errors.New("keine m3u8 URL gefunden")
	}

	return handleM3U8Mode(m3u8URL, outArg)
}

/* ───────────────────────────────
   Gemeinsame HLS/M3U8-Helper (MFC)
   ─────────────────────────────── */

func getWantedResolutionPlaylist(playlistURL string) (string, error) {
	// Holt eine URL; wenn MASTER, wähle beste Variante; wenn MEDIA, gib die URL zurück.
	resp, err := http.Get(playlistURL)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("HTTP %d beim Abruf der m3u8", resp.StatusCode)
	}

	playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
	if err != nil {
		return "", fmt.Errorf("m3u8 parse: %w", err)
	}
	if listType == m3u8.MEDIA {
		return playlistURL, nil
	}

	master := playlist.(*m3u8.MasterPlaylist)
	var bestURI string
	var bestWidth int
	var bestFramerate float64

	for _, v := range master.Variants {
		if v == nil {
			continue
		}
		// Resolution kommt als "WxH" – wir nutzen die Höhe als Vergleichswert.
		w := 0
		if v.Resolution != "" {
			parts := strings.Split(v.Resolution, "x")
			if len(parts) == 2 {
				if ww, err := strconv.Atoi(parts[1]); err == nil {
					w = ww
				}
			}
		}
		fr := 30.0
		if v.FrameRate > 0 {
			fr = v.FrameRate
		} else if strings.Contains(v.Name, "FPS:60") {
			fr = 60
		}
		if w > bestWidth || (w == bestWidth && fr > bestFramerate) {
			bestWidth = w
			bestFramerate = fr
			bestURI = v.URI
		}
	}
	if bestURI == "" {
		return "", errors.New("Master-Playlist ohne gültige Varianten")
	}

	// Absolutieren
	root := playlistURL[:strings.LastIndex(playlistURL, "/")+1]
	if strings.HasPrefix(bestURI, "http://") || strings.HasPrefix(bestURI, "https://") {
		return bestURI, nil
	}
	return root + bestURI, nil
}

func handleM3U8Mode(m3u8URL, outArg string) error {
	// Minimalvalidierung
	u, err := url.Parse(m3u8URL)
	if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
		return fmt.Errorf("ungültige URL: %q", m3u8URL)
	}

	// kleinen Check machen, ob abrufbar
	resp, err := http.Get(m3u8URL)
	if err != nil {
		return fmt.Errorf("Abruf fehlgeschlagen: %w", err)
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("HTTP %d beim Abruf der m3u8", resp.StatusCode)
	}

	// Ausgabedatei
	outFile := outArg
	if strings.TrimSpace(outFile) == "" {
		def := "mfc_" + time.Now().Format("20060102_150405") + ".mp4"
		fmt.Printf("Name der MP4-Datei (Enter für %s): ", def)
		outFile = readLine()
		if strings.TrimSpace(outFile) == "" {
			outFile = def
		}
	}

	// Überschreiben?
	if fileExists(outFile) {
		fmt.Print("Die Datei existiert bereits. Überschreiben? [y/N] ")
		a := strings.ToLower(strings.TrimSpace(readLine()))
		if a != "y" && a != "j" {
			return errors.New("abgebrochen")
		}
	}

	// ffmpeg copy-download
	fmt.Println("📦 Starte Download mit ffmpeg:", outFile)
	cmd := exec.Command(
		"ffmpeg",
		"-i", m3u8URL,
		"-c", "copy",
		outFile,
	)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("ffmpeg fehlgeschlagen: %w", err)
	}

	fmt.Println("✅ Download abgeschlossen:", outFile)
	return nil
}

/* ───────────────────────────────
   Kleine Helper für MFC
   ─────────────────────────────── */

func extractMFCUsername(input string) string {
	if strings.Contains(input, "myfreecams.com/#") {
		i := strings.Index(input, "#")
		if i >= 0 && i < len(input)-1 {
			return strings.TrimSpace(input[i+1:])
		}
		return ""
	}
	return strings.TrimSpace(input)
}

func readLine() string {
	r := bufio.NewReader(os.Stdin)
	s, _ := r.ReadString('\n')
	return strings.TrimRight(s, "\r\n")
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}
