package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sync/atomic"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/wader/goutubedl"
)

const downloadAndConvertTimeout = 30 * time.Minute
const telegramUploadThreshold = 512 * 1024 * 1024 // 512MB
const downloadProgressUpdateInterval = time.Second

type ConvertStartCallbackFunc func(ctx context.Context, videoCodecs, audioCodecs, convertActionsNeeded string)
type UpdateProgressPercentCallbackFunc func(progressStr string, progressPercent int)

type Downloader struct {
	ConvertStartFunc          ConvertStartCallbackFunc
	UpdateProgressPercentFunc UpdateProgressPercentCallbackFunc
}

type goYouTubeDLLogger struct{}

func (l goYouTubeDLLogger) Print(v ...interface{}) {
	fmt.Print("  yt-dlp dbg:")
	fmt.Println(v...)
}

type DownloadResult struct {
	Title    string
	FilePath string
	FileSize int64
}

// progressWriter wraps an io.Writer and tracks bytes written
type progressWriter struct {
	writer       io.Writer
	written      int64
	total        int64 // estimated total size, 0 if unknown
	callback     UpdateProgressPercentCallbackFunc
	lastUpdate   time.Time
	updateInterval time.Duration
}

func newProgressWriter(w io.Writer, total int64, callback UpdateProgressPercentCallbackFunc) *progressWriter {
	return &progressWriter{
		writer:         w,
		total:          total,
		callback:       callback,
		lastUpdate:     time.Now(),
		updateInterval: downloadProgressUpdateInterval,
	}
}

func (pw *progressWriter) Write(p []byte) (n int, err error) {
	n, err = pw.writer.Write(p)
	if n > 0 {
		atomic.AddInt64(&pw.written, int64(n))

		now := time.Now()
		if now.Sub(pw.lastUpdate) >= pw.updateInterval {
			pw.lastUpdate = now
			written := atomic.LoadInt64(&pw.written)

			if pw.total > 0 && pw.callback != nil {
				percent := int(float64(written) * 100 / float64(pw.total))
				if percent > 100 {
					percent = 100
				}
				pw.callback("⬇️ Downloading", percent)
			} else if pw.callback != nil {
				// Unknown total size, just show bytes downloaded
				pw.callback(fmt.Sprintf("⬇️ Downloaded %s", humanize.Bytes(uint64(written))), -1)
			}
		}
	}
	return n, err
}

func (pw *progressWriter) Written() int64 {
	return atomic.LoadInt64(&pw.written)
}

func (d *Downloader) downloadURL(dlCtx context.Context, url string) (*DownloadResult, error) {
	// Use 4K quality for saving, but fall back to best available
	result, err := goutubedl.New(dlCtx, url, goutubedl.Options{
		Type:              goutubedl.TypeSingle,
		DebugLog:          goYouTubeDLLogger{},
		MergeOutputFormat: "mkv",      // This handles VP9 properly. yt-dlp uses mp4 by default, which doesn't.
		SortingFormat:     "res:2160", // Prefer videos up to 4K (2160p)
	})
	if err != nil {
		return nil, fmt.Errorf("preparing download %q: %w", url, err)
	}

	// Create filename with date and timestamp format: 2025-11-22-{timestamp}.mkv
	now := time.Now()
	dateStr := now.Format("2006-01-02")
	timestamp := now.Unix()
	fileName := fmt.Sprintf("%s-%d.mkv", dateStr, timestamp)
	filePath := filepath.Join(params.SaveDir, fileName)

	// Check if file already exists, if so add a suffix
	if _, err := os.Stat(filePath); err == nil {
		for i := 1; i < 1000; i++ {
			fileName = fmt.Sprintf("%s-%d-%d.mkv", dateStr, timestamp, i)
			filePath = filepath.Join(params.SaveDir, fileName)
			if _, err := os.Stat(filePath); os.IsNotExist(err) {
				break
			}
		}
	}

	dlResult, err := result.Download(dlCtx, "")
	if err != nil {
		return nil, fmt.Errorf("downloading %q: %w", url, err)
	}
	defer dlResult.Close()

	// Create file
	file, err := os.Create(filePath)
	if err != nil {
		return nil, fmt.Errorf("creating file %q: %w", filePath, err)
	}
	defer file.Close()

	// Get estimated file size from format info
	var estimatedSize int64
	if len(result.Info.Formats) > 0 {
		// Try to get filesize from the selected format
		for _, f := range result.Info.Formats {
			if f.Filesize > 0 {
				fs := int64(f.Filesize)
				if fs > estimatedSize {
					estimatedSize = fs
				}
			} else if f.FilesizeApprox > 0 && estimatedSize == 0 {
				estimatedSize = int64(f.FilesizeApprox)
			}
		}
	}
	if estimatedSize == 0 && result.Info.Filesize > 0 {
		estimatedSize = int64(result.Info.Filesize)
	}
	if estimatedSize == 0 && result.Info.FilesizeApprox > 0 {
		estimatedSize = int64(result.Info.FilesizeApprox)
	}

	// Create progress writer
	pw := newProgressWriter(file, estimatedSize, d.UpdateProgressPercentFunc)

	// Copy data to file with progress tracking
	_, err = io.Copy(pw, dlResult)
	if err != nil {
		os.Remove(filePath)
		return nil, fmt.Errorf("writing to file %q: %w", filePath, err)
	}

	written := pw.Written()
	fmt.Printf("  saved to %s (%s)\n", filePath, humanize.Bytes(uint64(written)))

	return &DownloadResult{
		Title:    result.Info.Title,
		FilePath: filePath,
		FileSize: written,
	}, nil
}

func (d *Downloader) DownloadAndConvertURL(ctx context.Context, url, format string) (*DownloadResult, error) {
	return d.downloadURL(ctx, url)
}
