package particle

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/flowy-live/llink/internal/utils/flog"

	"cloud.google.com/go/firestore"

	"github.com/flowy-live/llink/internal/depot"
	"github.com/flowy-live/llink/internal/media"
)

const (
	SkipReasonNotMedia          = "not_media"
	SkipReasonAlreadyTranscoded = "already_transcoded"
	SkipReasonIOSPlayable       = "ios_playable"
)

// TranscodeResult reports the outcome of a single Transcode call. Skipped is
// true for any of the three idempotency short-circuits (with SkipReason set);
// Err is non-nil for real failures (download URL, ffmpeg, GCS upload, Firestore
// write). On success both flags are zero-value and TranscodedObjectID +
// OutputMimeType are populated.
type TranscodeResult struct {
	Skipped            bool
	SkipReason         string
	TranscodedObjectID string
	OutputMimeType     string
	Err                error
}

// Transcode produces an iOS-playable MP4/m4a derivative for media particles
// whose original mime type AVPlayer can't decode (notably the WebM the desktop
// recorder emits today). Skips when the particle is not of type media, when a
// transcoded variant has already been written, or when the source is already
// in an iOS-playable family.
//
// ffmpeg reads directly from the GCS signed URL and writes to a local temp
// file — `+faststart` requires seekable output, so a stdout pipe wouldn't
// work. The temp file is then streamed to GCS via depotSvc.CreateFromReader.
func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.DocumentSnapshot) TranscodeResult {
	var mp FirestoreMediaParticle
	if err := doc.DataTo(&mp); err != nil {
		flog.Error("transcode: unable to marshal particle data", "error", err)
		return TranscodeResult{Err: fmt.Errorf("unmarshal particle: %w", err)}
	}

	particleType, err := ParseParticleType(mp.Type)
	if err != nil {
		flog.Error("transcode: invalid particle type", "error", err)
		return TranscodeResult{Err: fmt.Errorf("parse type: %w", err)}
	}
	if particleType != TypeMedia {
		flog.Info("transcode: particle is not of type media")
		return TranscodeResult{Skipped: true, SkipReason: SkipReasonNotMedia}
	}

	if mp.Properties.TranscodedObjectId != "" {
		return TranscodeResult{Skipped: true, SkipReason: SkipReasonAlreadyTranscoded}
	}

	if media.IsIOSPlayableMime(mp.Properties.MimeType) {
		flog.Info("transcode: skipping because already playable on ios")
		return TranscodeResult{Skipped: true, SkipReason: SkipReasonIOSPlayable}
	}

	sourceURL, err := depotSvc.GetDownloadURL(ctx, mp.Properties.ObjectId)
	if err != nil {
		flog.Error("transcode: failed to get download URL", "error", err, "object_id", mp.Properties.ObjectId)
		return TranscodeResult{Err: fmt.Errorf("download URL: %w", err)}
	}

	networkID, err := NetworkIDFromParticlePath(doc.Ref.Path)
	if err != nil {
		flog.Error("transcode: failed to derive network id", "error", err, "path", doc.Ref.Path)
		return TranscodeResult{Err: fmt.Errorf("network id: %w", err)}
	}

	transcodeCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
	defer cancel()
	transcodeOutput, err := media.TranscodeToMp4(transcodeCtx, media.TranscodeInput{
		SourceURL: sourceURL,
		MimeType:  mp.Properties.MimeType,
	})
	if err != nil {
		flog.Error("transcode: ffmpeg failed", "error", err)
		return TranscodeResult{Err: fmt.Errorf("ffmpeg: %w", err)}
	}
	defer os.Remove(transcodeOutput.TempLocalFilePath)

	f, err := os.Open(transcodeOutput.TempLocalFilePath)
	if err != nil {
		flog.Error("transcode: failed to open transcoded file", "error", err, "path", transcodeOutput.TempLocalFilePath)
		return TranscodeResult{Err: fmt.Errorf("open temp file: %w", err)}
	}
	defer f.Close()

	newObj, err := depotSvc.CreateFromReader(ctx, depot.CreateFromReaderInput{
		Prefix:      networkID,
		Name:        "transcoded" + transcodeOutput.OutputExt,
		ContentType: transcodeOutput.OutputMimeType,
	}, f)
	if err != nil {
		flog.Error("transcode: failed to upload transcoded object", "error", err, "particleID", doc.Ref.ID)
		return TranscodeResult{Err: fmt.Errorf("upload: %w", err)}
	}

	if _, err := doc.Ref.Set(ctx, map[string]interface{}{
		"properties": map[string]interface{}{
			"transcoded_object_id": newObj.ID,
			"transcoded_mime_type": transcodeOutput.OutputMimeType,
		},
	}, firestore.MergeAll); err != nil {
		flog.Error("transcode: failed to update particle in firestore", "error", err, "particleID", doc.Ref.ID)
		return TranscodeResult{Err: fmt.Errorf("firestore update: %w", err)}
	}

	flog.Info("transcoded media particle", "particleID", doc.Ref.ID, "transcoded_object_id", newObj.ID, "mime", transcodeOutput.OutputMimeType)
	return TranscodeResult{
		TranscodedObjectID: newObj.ID,
		OutputMimeType:     transcodeOutput.OutputMimeType,
	}
}

// NetworkIDFromParticlePath extracts the network id from a Firestore particle
// document path. Particles live at `networks/{network_id}/children/.../children/{id}`
// at arbitrary nesting depth, so the network id is always the segment directly
// following the "networks" collection name in the full resource path.
func NetworkIDFromParticlePath(path string) (string, error) {
	segments := strings.Split(path, "/")
	for i, seg := range segments {
		if seg == "networks" && i+1 < len(segments) {
			return segments[i+1], nil
		}
	}
	return "", fmt.Errorf("no networks segment in path: %s", path)
}
