package plugin_mp4

import (
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/gobwas/ws/wsutil"
	"m7s.live/v5"
	v5 "m7s.live/v5/pkg"
	"m7s.live/v5/pkg/codec"
	"m7s.live/v5/plugin/mp4/pb"
	pkg "m7s.live/v5/plugin/mp4/pkg"
	"m7s.live/v5/plugin/mp4/pkg/box"
	rtmp "m7s.live/v5/plugin/rtmp/pkg"
)

type MediaContext struct {
	io.Writer
	conn   net.Conn
	wto    time.Duration
	ws     bool
	buffer []byte
}

func (m *MediaContext) Write(p []byte) (n int, err error) {
	if m.ws {
		m.buffer = append(m.buffer, p...)
		return len(p), nil
	}
	if m.conn != nil && m.wto > 0 {
		m.conn.SetWriteDeadline(time.Now().Add(m.wto))
	}
	return m.Writer.Write(p)
}

func (m *MediaContext) Flush() (err error) {
	if m.ws {
		if m.wto > 0 {
			m.conn.SetWriteDeadline(time.Now().Add(m.wto))
		}
		err = wsutil.WriteServerBinary(m.conn, m.buffer)
		m.buffer = m.buffer[:0]
	}
	return
}

type MP4Plugin struct {
	pb.UnimplementedApiServer
	m7s.Plugin
	BeforeDuration           time.Duration `default:"30s" desc:"事件录像提前时长，不配置则默认30s"`
	AfterDuration            time.Duration `default:"30s" desc:"事件录像结束时长，不配置则默认30s"`
	RecordFileExpireDays     int           `desc:"录像自动删除的天数,0或未设置表示不自动删除"`
	DiskMaxPercent           float64       `default:"90" desc:"硬盘使用百分之上限值，超上限后触发报警，并停止当前所有磁盘写入动作。"`
	AutoOverWriteDiskPercent float64       `default:"0" desc:"自动覆盖功能磁盘占用上限值，超过上限时连续录像自动删除日有录像，事件录像自动删除非重要事件录像，删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
	ExceptionPostUrl         string        `desc:"第三方异常上报地址"`
	EventRecordFilePath      string        `desc:"事件录像存放地址"`
}

const defaultConfig m7s.DefaultYaml = `publish:
  speed: 1`

// var exceptionChannel = make(chan *Exception)
var _ = m7s.InstallPlugin[MP4Plugin](defaultConfig, &pb.Api_ServiceDesc, pb.RegisterApiHandler, pkg.NewPuller, pkg.NewRecorder)

func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
	return map[string]http.HandlerFunc{
		"/download/{streamPath...}": p.download,
	}
}

func (p *MP4Plugin) OnInit() (err error) {
	if p.DB != nil && p.AutoOverWriteDiskPercent > 0 {
		err = p.DB.AutoMigrate(&Exception{})
		var deleteRecordTask DeleteRecordTask
		deleteRecordTask.DB = p.DB
		deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent
		deleteRecordTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
		deleteRecordTask.RecordFileExpireDays = p.RecordFileExpireDays
		deleteRecordTask.plugin = p
		p.AddTask(&deleteRecordTask)
	}
	// go func() { //处理所有异常，录像中断异常、录像读取异常、录像导出文件中断、磁盘容量低于阈值异常、磁盘异常
	// 	for exception := range exceptionChannel {
	// 		p.SendToThirdPartyAPI(exception)
	// 	}
	// }()
	_, port, _ := strings.Cut(p.GetCommonConf().HTTP.ListenAddr, ":")
	if port == "80" {
		p.PlayAddr = append(p.PlayAddr, "http://{hostName}/mp4/{streamPath}.mp4")
	} else if port != "" {
		p.PlayAddr = append(p.PlayAddr, fmt.Sprintf("http://{hostName}:%s/mp4/{streamPath}.mp4", port))
	}
	_, port, _ = strings.Cut(p.GetCommonConf().HTTP.ListenAddrTLS, ":")
	if port == "443" {
		p.PlayAddr = append(p.PlayAddr, "https://{hostName}/mp4/{streamPath}.mp4")
	} else if port != "" {
		p.PlayAddr = append(p.PlayAddr, fmt.Sprintf("https://{hostName}:%s/mp4/{streamPath}.mp4", port))
	}
	return
}

func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".mp4")
	if r.URL.RawQuery != "" {
		streamPath += "?" + r.URL.RawQuery
	}
	sub, err := p.Subscribe(r.Context(), streamPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	sub.RemoteAddr = r.RemoteAddr
	var ctx MediaContext
	ctx.conn, err = sub.CheckWebSocket(w, r)
	if err != nil {
		return
	}
	ctx.wto = p.GetCommonConf().WriteTimeout
	if ctx.conn == nil {
		w.Header().Set("Transfer-Encoding", "chunked")
		w.Header().Set("Content-Type", "video/mp4")
		w.WriteHeader(http.StatusOK)
		if hijacker, ok := w.(http.Hijacker); ok && ctx.wto > 0 {
			ctx.conn, _, _ = hijacker.Hijack()
			ctx.conn.SetWriteDeadline(time.Now().Add(ctx.wto))
			ctx.Writer = ctx.conn
		} else {
			ctx.Writer = w
			w.(http.Flusher).Flush()
		}
	} else {
		ctx.ws = true
		ctx.Writer = ctx.conn
	}

	muxer := pkg.NewMuxer(pkg.FLAG_FRAGMENT)
	err = muxer.WriteInitSegment(&ctx)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	var offsetAudio, offsetVideo = 1, 5
	var audio, video *pkg.Track
	var nextFragmentId uint32
	if sub.Publisher.HasVideoTrack() && sub.SubVideo {
		v := sub.Publisher.VideoTrack.AVTrack
		if err = v.WaitReady(); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		var codecID box.MP4_CODEC_TYPE
		switch v.ICodecCtx.FourCC() {
		case codec.FourCC_H264:
			codecID = box.MP4_CODEC_H264
		case codec.FourCC_H265:
			codecID = box.MP4_CODEC_H265
		}
		video = muxer.AddTrack(codecID)
		video.Timescale = 1000
		video.Samplelist = []box.Sample{
			{
				Offset:    0,
				Data:      nil,
				Size:      0,
				Timestamp: 0,
				Duration:  0,
				KeyFrame:  true,
			},
		}
		switch v.ICodecCtx.FourCC() {
		case codec.FourCC_H264:
			h264Ctx := v.ICodecCtx.GetBase().(*codec.H264Ctx)
			video.ExtraData = h264Ctx.Record
			video.Width = uint32(h264Ctx.Width())
			video.Height = uint32(h264Ctx.Height())
		case codec.FourCC_H265:
			h265Ctx := v.ICodecCtx.GetBase().(*codec.H265Ctx)
			video.ExtraData = h265Ctx.Record
			video.Width = uint32(h265Ctx.Width())
			video.Height = uint32(h265Ctx.Height())
		}
	}

	if sub.Publisher.HasAudioTrack() && sub.SubAudio {
		a := sub.Publisher.AudioTrack.AVTrack
		if err = a.WaitReady(); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		var codecID box.MP4_CODEC_TYPE
		switch a.ICodecCtx.FourCC() {
		case codec.FourCC_MP4A:
			codecID = box.MP4_CODEC_AAC
		}
		audio = muxer.AddTrack(codecID)
		audio.Timescale = 1000
		audioCtx := a.ICodecCtx.(v5.IAudioCodecCtx)
		audio.SampleRate = uint32(audioCtx.GetSampleRate())
		audio.ChannelCount = uint8(audioCtx.GetChannels())
		audio.SampleSize = uint16(audioCtx.GetSampleSize())
		audio.Samplelist = []box.Sample{
			{
				Offset:    0,
				Data:      nil,
				Size:      0,
				Timestamp: 0,
				Duration:  0,
				KeyFrame:  true,
			},
		}
		switch a.ICodecCtx.FourCC() {
		case codec.FourCC_MP4A:
			offsetAudio = 2
			audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes
		default:
			offsetAudio = 1
		}
	}
	err = muxer.WriteMoov(&ctx)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if ctx.ws {
		ctx.Flush()
	}
	m7s.PlayBlock(sub, func(frame *rtmp.RTMPAudio) (err error) {
		bs := frame.Memory.ToBytes()
		if offsetAudio == 2 && bs[1] == 0 {
			return nil
		}
		if audio.Samplelist[0].Data != nil {
			audio.Samplelist[0].Duration = sub.AudioReader.AbsTime - audio.Samplelist[0].Timestamp
			nextFragmentId++
			// Create moof box for this track
			moof := audio.MakeMoof(nextFragmentId)
			// Create mdat box for this track
			mdat := box.CreateDataBox(box.TypeMDAT, audio.Samplelist[0].Data)
			box.WriteTo(&ctx, moof, mdat)
			if ctx.ws {
				err = ctx.Flush()
			}
		}
		audio.Samplelist[0].Timestamp = sub.AudioReader.AbsTime
		audio.Samplelist[0].Data = bs[offsetAudio:]
		audio.Samplelist[0].Size = len(audio.Samplelist[0].Data)
		return
	}, func(frame *rtmp.RTMPVideo) (err error) {
		bs := frame.Memory.ToBytes()
		if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced {
			switch bs[0] & 0b1111 {
			case rtmp.PacketTypeCodedFrames:
				offsetVideo = 8
			case rtmp.PacketTypeSequenceStart:
				return nil
			}
		} else {
			if bs[1] == 0 {
				return nil
			}
			offsetVideo = 5
		}
		if video.Samplelist[0].Data != nil {
			video.Samplelist[0].Duration = sub.VideoReader.AbsTime - video.Samplelist[0].Timestamp
			nextFragmentId++
			// Create moof box for this track
			moof := video.MakeMoof(nextFragmentId)
			// Create mdat box for this track
			mdat := box.CreateDataBox(box.TypeMDAT, video.Samplelist[0].Data)
			box.WriteTo(&ctx, moof, mdat)
			if ctx.ws {
				err = ctx.Flush()
			}
		}
		video.Samplelist[0].Data = bs[offsetVideo:]
		video.Samplelist[0].Size = len(bs) - offsetVideo
		video.Samplelist[0].Timestamp = sub.VideoReader.AbsTime
		video.Samplelist[0].CTS = frame.CTS
		video.Samplelist[0].KeyFrame = sub.VideoReader.Value.IDR
		return
	})
}
