package main

import (
	"backend/config"
	"backend/queries"
	"backend/typesdefs"
	"backend/utils"
	"fmt"
	"image/jpeg"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/nfnt/resize"
	"github.com/nickalie/go-webpbin"
	"github.com/schollz/progressbar/v3"
)

func AddToVideoProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML, p *[]typesdefs.MediaToProcess) {

	miogestVideos := []string{}
	if annuncio.AMMedias != nil {
		for _, media := range *annuncio.AMMedias {
			if media.AMVideo != nil && media.AMVideo.Url != nil {
				if strings.Contains(*media.AMVideo.Url, "video.miogest") {
					miogestVideos = append(miogestVideos, *media.AMVideo.Url)
				}

			}
		}
	}

	for idx, url := range miogestVideos {
		*p = append(*p, typesdefs.MediaToProcess{
			Codice: tmp.Codice,
			Url:    url,
			Order:  idx,
		})
	}
}

func ProcessVideoPool(toProcess *[]typesdefs.MediaToProcess) ([]typesdefs.UploadedMedia, error) {
	if len(*toProcess) == 0 {
		log.Println("No new videos to process.")
		return []typesdefs.UploadedMedia{}, nil
	}
	var (
		ProcessedVideos []typesdefs.ProcessedMedia
		UploadedVideos  []typesdefs.UploadedMedia
		mu              sync.Mutex
		wg              sync.WaitGroup
	)
	totalToProcess := len(*toProcess)
	bar := progressbar.Default(int64(totalToProcess), "Processing videos")
	sem := make(chan struct{}, config.Cfg.Videos.ConcurrentLimit)

	// clear videos folder
	err := clearVideoFolder()
	if err != nil {
		log.Fatalf("Failed to initialize image processing: %v", err)
	}

	// process videos
	for _, video := range *toProcess {
		sem <- struct{}{} // acquire semaphore
		wg.Add(1)
		go func(video typesdefs.MediaToProcess) {
			defer wg.Done()
			defer func() { <-sem }() // release semaphore
			videoPath := filepath.Join(config.Cfg.Videos.Path, fmt.Sprintf("%s_%d%s", video.Codice, video.Order, filepath.Ext(video.Url)))
			err := downloadVideo(video.Url, videoPath)
			if err != nil {
				log.Printf("Failed to download video %s: %v", video.Url, err)
				return // Exit goroutine on download failure
			}
			// Transcode and generate thumbnail
			mp4Path, thumbnailPath, err := VideoConversion(videoPath)
			if err != nil {
				log.Printf("Failed to convert video %s: %v", videoPath, err)
				return
			}
			mu.Lock()
			ProcessedVideos = append(ProcessedVideos, typesdefs.ProcessedMedia{
				Codice:       video.Codice,
				Order:        video.Order,
				Path:         mp4Path,
				ThumnailPath: thumbnailPath,
				OGUrl:        video.Url,
			})
			bar.Add(1)
			mu.Unlock()
		}(video)
	}
	wg.Wait()
	bar.Finish()
	fmt.Println()

	totalToUpload := len(ProcessedVideos)
	uploadBar := progressbar.Default(int64(totalToUpload), "Uploading videos")
	uploadSem := make(chan struct{}, config.Cfg.Videos.ConcurrentLimit)
	var uploadWg sync.WaitGroup
	for _, video := range ProcessedVideos {
		uploadSem <- struct{}{} // acquire semaphore
		uploadWg.Add(1)
		go func(video typesdefs.ProcessedMedia) {
			defer uploadWg.Done()
			defer func() { <-uploadSem }() // release semaphore

			videoId, err := UploadFile(video.Path, "video/mp4", "videos")
			if err != nil {
				log.Printf("failed to upload video: %v", err)
				return
			}
			thumbId, err := UploadFile(video.ThumnailPath, "image/webp", "videos")
			if err != nil {
				log.Printf("failed to upload thumbnail: %v", err)
				return
			}
			mu.Lock()
			UploadedVideos = append(UploadedVideos, typesdefs.UploadedMedia{
				Codice:  video.Codice,
				Order:   video.Order,
				MediaId: videoId,
				ThumbId: thumbId,
				OGUrl:   video.OGUrl,
			})
			uploadBar.Add(1)
			mu.Unlock()
		}(video)
	}
	uploadWg.Wait()
	uploadBar.Finish()
	fmt.Println()

	return UploadedVideos, nil
}

func clearVideoFolder() error {
	err := utils.SafeRemoveAll(config.Cfg.Videos.Path)
	if err != nil {
		return err
	}
	err = os.Mkdir(config.Cfg.Videos.Path, 0755)
	if err != nil {
		return err
	}
	return nil
}

func downloadVideo(url, filename string) error {
	client := &http.Client{
		Timeout: 5 * time.Minute, // Adjust based on expected video sizes
	}

	response, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("failed to download video: %w", err)
	}
	defer response.Body.Close()

	// Check HTTP status
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to download video: HTTP %d", response.StatusCode)
	}

	// Optional: Validate Content-Type
	contentType := response.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "video/") && !strings.HasPrefix(contentType, "application/") {
		log.Printf("Warning: unexpected content type %s for %s", contentType, url)
	}

	file, err := os.Create(filename)
	if err != nil {
		return fmt.Errorf("failed to create video file: %w", err)
	}
	defer file.Close()

	_, err = io.Copy(file, response.Body)
	if err != nil {
		return fmt.Errorf("failed to save video file: %w", err)
	}

	return nil
}

// VideoConversion transcodes video and generates thumbnail
func VideoConversion(input string) (string, string, error) {
	outputdir := filepath.Dir(input)
	basename := filepath.Base(input)
	ext := filepath.Ext(basename)
	nameWithoutExt := basename[0 : len(basename)-len(ext)]

	tempVideo := filepath.Join(outputdir, nameWithoutExt+"_temp.mp4")
	videoOutput := filepath.Join(outputdir, nameWithoutExt+".mp4")
	thumbOutput := filepath.Join(outputdir, "thumbnail-"+nameWithoutExt+".webp")

	// 1. Transcode video to H.264/AAC MP4 (web-optimized)
	//log.Println("Transcoding video:", input)
	err := transcodeVideo(input, tempVideo)
	if err != nil {
		return "", "", fmt.Errorf("failed to transcode video: %w", err)
	}

	// 2. Extract thumbnail from video at 1 second
	//log.Println("Extracting thumbnail from:", input)
	err = extractThumbnail(input, thumbOutput)
	if err != nil {
		return "", "", fmt.Errorf("failed to extract thumbnail: %w", err)
	}
	os.Remove(input)
	err = os.Rename(tempVideo, videoOutput)
	if err != nil {
		return "", "", fmt.Errorf("failed to rename converted video: %w", err)
	}

	return videoOutput, thumbOutput, nil
}

// transcodeVideo converts video to web-optimized MP4
func transcodeVideo(input, output string) error {
	// FFmpeg command for web optimization:
	// -c:v libx264: H.264 codec (widely supported)
	// -preset fast: encoding speed/quality tradeoff
	// -crf 23: quality (lower = better, 18-28 recommended)
	// -c:a aac: AAC audio codec
	// -b:a 128k: audio bitrate
	// -movflags +faststart: enable streaming before full download
	// -vf scale=1280:-2: scale to max width 1280, maintain aspect ratio
	cmd := exec.Command("ffmpeg",
		"-i", input,
		"-c:v", "libx264",
		"-preset", "fast",
		"-crf", "23",
		"-c:a", "aac",
		"-b:a", "128k",
		"-movflags", "+faststart",
		"-vf", "scale=min(1280\\,iw):-2", // Escape comma for cross-platform
		"-y",
		output,
	)

	output_bytes, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("ffmpeg transcode failed: %w, output: %s", err, string(output_bytes))
	}

	return nil
}

// extractThumbnail generates a WebP thumbnail from video
func extractThumbnail(input, output string) error {
	// Extract frame at 1 second as JPEG to temp file
	tempJpeg := output + ".tmp.jpg"
	defer os.Remove(tempJpeg)

	cmd := exec.Command("ffmpeg",
		"-i", input,
		"-ss", "00:00:01", // seek to 1 second
		"-vframes", "1", // extract 1 frame
		"-q:v", "2", // JPEG quality (2-31, lower = better)
		"-y",
		tempJpeg,
	)

	output_bytes, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("ffmpeg thumbnail extraction failed: %w, output: %s", err, string(output_bytes))
	}

	// Convert JPEG to WebP thumbnail
	f, err := os.Open(tempJpeg)
	if err != nil {
		return fmt.Errorf("failed to open temp jpeg: %w", err)
	}
	defer f.Close()

	img, err := jpeg.Decode(f)
	if err != nil {
		return fmt.Errorf("failed to decode jpeg: %w", err)
	}

	// Resize to thumbnail (320px width)
	resizedImg := resize.Resize(320, 0, img, resize.Lanczos3)

	out, err := os.Create(output)
	if err != nil {
		return fmt.Errorf("failed to create thumbnail: %w", err)
	}
	defer out.Close()

	err = webpbin.Encode(out, resizedImg)
	if err != nil {
		return fmt.Errorf("failed to encode webp: %w", err)
	}

	return nil
}

func FallbackVideo(c echo.Context) error {
	// Serve a fallback image if the requested image is not found
	fallbackPath := "fallback.webp" // Path to your fallback image
	if _, err := os.Stat(fallbackPath); os.IsNotExist(err) {
		return c.String(http.StatusNotFound, "Fallback image not found")
	}
	return c.File(fallbackPath)
}

type videRef struct {
	Og_url string
	Video  string
	Thumb  string
}

func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) {
	currentBucketState, err := ListBucket("videos")
	if err != nil {
		return nil, fmt.Errorf("failed to list bucket: %w", err)
	}

	//make a list of bucket ids
	bucketIdsMap := make(map[string]bool)
	for _, item := range currentBucketState {
		bucketIdsMap[item.ID] = true
	}

	db_videos, err := queries.GetVideosFromDB()
	if err != nil {
		return nil, err
	}

	dbVideosMap := make(map[string][]videRef) // Convert slice to map for faster lookup
	for _, ref := range db_videos {
		dbVideosMap[ref.Codice] = append(dbVideosMap[ref.Codice], videRef{
			Og_url: ref.Og_url,
			Video:  ref.Video,
			Thumb:  ref.Thumb,
		})
	}

	var codiciToProcess []string
	for _, annuncio := range annunci.Annuncio {

		if annuncio.Codice != nil && annuncio.AMMedias != nil && len(*annuncio.AMMedias) > 0 {
			mediaUrls := []string{}
			for _, media := range *annuncio.AMMedias {
				if media.AMVideo != nil && media.AMVideo.Url != nil {
					if strings.Contains(*media.AMVideo.Url, "video.miogest") {
						mediaUrls = append(mediaUrls, *media.AMVideo.Url)
					}
				}
			}
			if len(mediaUrls) == 0 {
				continue
			}

			refVideos, existsInDb := dbVideosMap[*annuncio.Codice]
			if !existsInDb {
				codiciToProcess = append(codiciToProcess, *annuncio.Codice)
				continue
			}
			needProcess := false

			for _, media := range *annuncio.AMMedias {
				if media.AMVideo == nil || media.AMVideo.Url == nil {
					continue
				}
				if !strings.Contains(*media.AMVideo.Url, "video.miogest") {
					continue
				}

				found := false
				for _, ref := range refVideos {
					if *media.AMVideo.Url == ref.Og_url {
						if bucketIdsMap[ref.Video] && bucketIdsMap[ref.Thumb] {
							found = true
							break
						}
					}
				}
				if !found {
					needProcess = true
					break
				}

			}
			if needProcess {
				codiciToProcess = append(codiciToProcess, *annuncio.Codice)
			}
		}
	}

	return codiciToProcess, nil

}

func ForceClearVideos(codici []string) error {
	var idsToDelete []string
	for _, codice := range codici {
		refs, err := queries.GetCodiceVideosFromDB(codice)
		if err != nil {
			return fmt.Errorf("failed to get videos by codice %s: %w", codice, err)
		}
		for _, ref := range refs {
			idsToDelete = append(idsToDelete, ref.Video)
			idsToDelete = append(idsToDelete, ref.Thumb)
		}

	}
	err := DeleteIdsFromStorage(idsToDelete)
	if err != nil {
		return fmt.Errorf("failed to delete videos from storage: %w", err)
	}
	for _, codice := range codici {
		err := queries.ClearVideosRefsByCodice(codice)
		if err != nil {
			return fmt.Errorf("failed to clear videos by codice %s: %w", codice, err)
		}
	}
	return nil
}
