package directstream

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"seanime/internal/api/anilist"
	"seanime/internal/library/anime"
	"seanime/internal/mkvparser"
	"seanime/internal/nativeplayer"
	"seanime/internal/util"
	"seanime/internal/util/result"
	"time"

	"github.com/google/uuid"
	"github.com/samber/mo"
)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Local File
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var _ Stream = (*LocalFileStream)(nil)

// LocalFileStream is a stream that is a local file.
type LocalFileStream struct {
	BaseStream
	localFile *anime.LocalFile
}

func (s *LocalFileStream) newReader() (io.ReadSeekCloser, error) {
	r, err := os.OpenFile(s.localFile.Path, os.O_RDONLY, 0)
	if err != nil {
		return nil, err
	}

	return r, nil
}

func (s *LocalFileStream) Type() nativeplayer.StreamType {
	return nativeplayer.StreamTypeFile
}

func (s *LocalFileStream) LoadContentType() string {
	s.contentTypeOnce.Do(func() {
		// No need to pass a reader because we are not going to read the file
		// Get the mime type from the file extension
		s.contentType = loadContentType(s.localFile.Path)
	})

	return s.contentType
}

func (s *LocalFileStream) LoadPlaybackInfo() (ret *nativeplayer.PlaybackInfo, err error) {
	s.playbackInfoOnce.Do(func() {
		if s.localFile == nil {
			s.playbackInfo = &nativeplayer.PlaybackInfo{}
			err = fmt.Errorf("local file is not set")
			s.playbackInfoErr = err
			return
		}

		// Open the file
		fr, err := s.newReader()
		if err != nil {
			s.logger.Error().Err(err).Msg("directstream(file): Failed to open local file")
			s.manager.preStreamError(s, fmt.Errorf("cannot stream local file: %w", err))
			return
		}

		// Close the file when done
		defer func() {
			if closer, ok := fr.(io.Closer); ok {
				s.logger.Trace().Msg("directstream(file): Closing local file reader")
				_ = closer.Close()
			} else {
				s.logger.Trace().Msg("directstream(file): Local file reader does not implement io.Closer")
			}
		}()

		// Get the file size
		size, err := fr.Seek(0, io.SeekEnd)
		if err != nil {
			s.logger.Error().Err(err).Msg("directstream(file): Failed to get file size")
			s.manager.preStreamError(s, fmt.Errorf("failed to get file size: %w", err))
			return
		}
		_, _ = fr.Seek(0, io.SeekStart)

		id := uuid.New().String()

		var entryListData *anime.EntryListData
		if animeCollection, ok := s.manager.animeCollection.Get(); ok {
			if listEntry, ok := animeCollection.GetListEntryFromAnimeId(s.media.ID); ok {
				entryListData = anime.NewEntryListData(listEntry)
			}
		}

		playbackInfo := nativeplayer.PlaybackInfo{
			ID:                id,
			StreamType:        s.Type(),
			MimeType:          s.LoadContentType(),
			StreamUrl:         "{{SERVER_URL}}/api/v1/directstream/stream?id=" + id,
			ContentLength:     size,
			MkvMetadata:       nil,
			MkvMetadataParser: mo.None[*mkvparser.MetadataParser](),
			Episode:           s.episode,
			Media:             s.media,
			EntryListData:     entryListData,
		}

		// If the content type is an EBML content type, we can create a metadata parser
		if isEbmlContent(s.LoadContentType()) {

			parserKey := util.Base64EncodeStr(s.localFile.Path)

			parser, ok := s.manager.parserCache.Get(parserKey)
			if !ok {
				parser = mkvparser.NewMetadataParser(fr, s.logger)
				s.manager.parserCache.SetT(parserKey, parser, 2*time.Hour)
			}

			metadata := parser.GetMetadata(context.Background())
			if metadata.Error != nil {
				s.logger.Error().Err(metadata.Error).Msg("directstream(torrent): Failed to get metadata")
				s.manager.preStreamError(s, fmt.Errorf("failed to get metadata: %w", metadata.Error))
				s.playbackInfoErr = fmt.Errorf("failed to get metadata: %w", metadata.Error)
				return
			}

			playbackInfo.MkvMetadata = metadata
			playbackInfo.MkvMetadataParser = mo.Some(parser)
		}

		s.playbackInfo = &playbackInfo
	})

	return s.playbackInfo, s.playbackInfoErr
}

func (s *LocalFileStream) GetAttachmentByName(filename string) (*mkvparser.AttachmentInfo, bool) {
	return getAttachmentByName(s.manager.playbackCtx, s, filename)
}

func (s *LocalFileStream) GetStreamHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s.logger.Trace().Str("method", r.Method).Msg("directstream: Received request")

		defer func() {
			s.logger.Trace().Msg("directstream: Request finished")
		}()

		if r.Method == http.MethodHead {
			// Get the file size
			fileInfo, err := os.Stat(s.localFile.Path)
			if err != nil {
				s.logger.Error().Msg("directstream: Failed to get file info")
				http.Error(w, "Failed to get file info", http.StatusInternalServerError)
				return
			}

			// Set the content length
			w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
			w.Header().Set("Content-Type", s.LoadContentType())
			w.Header().Set("Accept-Ranges", "bytes")
			w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", s.localFile.Path))
			w.WriteHeader(http.StatusOK)
		} else {
			ServeLocalFile(w, r, s)
		}
	})
}

func ServeLocalFile(w http.ResponseWriter, r *http.Request, lfStream *LocalFileStream) {
	playbackInfo, err := lfStream.LoadPlaybackInfo()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	size := playbackInfo.ContentLength

	if isThumbnailRequest(r) {
		reader, err := lfStream.newReader()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		ra, ok := handleRange(w, r, reader, lfStream.localFile.Path, size)
		if !ok {
			return
		}
		serveContentRange(w, r, r.Context(), reader, lfStream.localFile.Path, size, playbackInfo.MimeType, ra)
		return
	}

	if lfStream.serveContentCancelFunc != nil {
		lfStream.serveContentCancelFunc()
	}

	ct, cancel := context.WithCancel(lfStream.manager.playbackCtx)
	lfStream.serveContentCancelFunc = cancel

	reader, err := lfStream.newReader()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer reader.Close()

	ra, ok := handleRange(w, r, reader, lfStream.localFile.Path, size)
	if !ok {
		return
	}

	if _, ok := playbackInfo.MkvMetadataParser.Get(); ok {
		// Start a subtitle stream from the current position
		subReader, err := lfStream.newReader()
		if err != nil {
			lfStream.logger.Error().Err(err).Msg("directstream: Failed to create subtitle reader")
			http.Error(w, "Failed to create subtitle reader", http.StatusInternalServerError)
			return
		}
		go lfStream.StartSubtitleStream(lfStream, lfStream.manager.playbackCtx, subReader, ra.Start)
	}

	serveContentRange(w, r, ct, reader, lfStream.localFile.Path, size, playbackInfo.MimeType, ra)
}

type PlayLocalFileOptions struct {
	ClientId   string
	Path       string
	LocalFiles []*anime.LocalFile
}

// PlayLocalFile is used by a module to load a new torrent stream.
func (m *Manager) PlayLocalFile(ctx context.Context, opts PlayLocalFileOptions) error {
	m.playbackMu.Lock()
	defer m.playbackMu.Unlock()

	animeCollection, ok := m.animeCollection.Get()
	if !ok {
		return fmt.Errorf("cannot play local file, anime collection is not set")
	}

	// Get the local file
	var lf *anime.LocalFile
	for _, l := range opts.LocalFiles {
		if util.NormalizePath(l.Path) == util.NormalizePath(opts.Path) {
			lf = l
			break
		}
	}

	if lf == nil {
		return fmt.Errorf("cannot play local file, could not find local file: %s", opts.Path)
	}

	if lf.MediaId == 0 {
		return fmt.Errorf("local file has not been matched to a media: %s", opts.Path)
	}

	mId := lf.MediaId
	var media *anilist.BaseAnime
	listEntry, ok := animeCollection.GetListEntryFromAnimeId(mId)
	if ok {
		media = listEntry.Media
	}

	if media == nil {
		return fmt.Errorf("media not found in anime collection: %d", mId)
	}

	episodeCollection, err := anime.NewEpisodeCollectionFromLocalFiles(ctx, anime.NewEpisodeCollectionFromLocalFilesOptions{
		LocalFiles:       opts.LocalFiles,
		Media:            media,
		AnimeCollection:  animeCollection,
		Platform:         m.platform,
		MetadataProvider: m.metadataProvider,
		Logger:           m.Logger,
	})
	if err != nil {
		return fmt.Errorf("cannot play local file, could not create episode collection: %w", err)
	}

	var episode *anime.Episode
	for _, e := range episodeCollection.Episodes {
		if e.LocalFile != nil && util.NormalizePath(e.LocalFile.Path) == util.NormalizePath(lf.Path) {
			episode = e
			break
		}
	}

	if episode == nil {
		return fmt.Errorf("cannot play local file, could not find episode for local file: %s", opts.Path)
	}

	stream := &LocalFileStream{
		localFile: lf,
		BaseStream: BaseStream{
			manager:               m,
			logger:                m.Logger,
			clientId:              opts.ClientId,
			filename:              filepath.Base(lf.Path),
			media:                 media,
			episode:               episode,
			episodeCollection:     episodeCollection,
			subtitleEventCache:    result.NewResultMap[string, *mkvparser.SubtitleEvent](),
			activeSubtitleStreams: result.NewResultMap[string, *SubtitleStream](),
		},
	}

	m.loadStream(stream)

	return nil
}
