package rtsp

import (
	"errors"
	"fmt"
	"net"
	"slices"
	"strings"
	"time"

	"github.com/bluenviron/gortsplib/v5"
	rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth"
	"github.com/bluenviron/gortsplib/v5/pkg/base"
	"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
	"github.com/google/uuid"

	"github.com/bluenviron/mediamtx/internal/auth"
	"github.com/bluenviron/mediamtx/internal/conf"
	"github.com/bluenviron/mediamtx/internal/defs"
	"github.com/bluenviron/mediamtx/internal/externalcmd"
	"github.com/bluenviron/mediamtx/internal/hooks"
	"github.com/bluenviron/mediamtx/internal/logger"
	"github.com/bluenviron/mediamtx/internal/protocols/rtsp"
)

func absoluteURL(req *base.Request, v string) string {
	if strings.HasPrefix(v, "/") {
		ur := base.URL{
			Scheme: req.URL.Scheme,
			Host:   req.URL.Host,
			Path:   v,
		}
		return ur.String()
	}

	return v
}

func tunnelLabel(t gortsplib.Tunnel) string {
	switch t {
	case gortsplib.TunnelHTTP:
		return "http"
	case gortsplib.TunnelWebSocket:
		return "websocket"
	case gortsplib.TunnelNone:
		return "none"
	}
	return "unknown"
}

type connParent interface {
	logger.Writer
	findSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session
}

type conn struct {
	isTLS               bool
	rtspAddress         string
	authMethods         []rtspauth.VerifyMethod
	readTimeout         conf.Duration
	runOnConnect        string
	runOnConnectRestart bool
	runOnDisconnect     string
	externalCmdPool     *externalcmd.Pool
	pathManager         serverPathManager
	rconn               *gortsplib.ServerConn
	rserver             *gortsplib.Server
	parent              connParent

	uuid             uuid.UUID
	created          time.Time
	onDisconnectHook func()
}

func (c *conn) initialize() {
	c.uuid = uuid.New()
	c.created = time.Now()

	c.Log(logger.Info, "opened")

	desc := defs.APIPathSourceOrReader{
		Type: func() string {
			if c.isTLS {
				return "rtspsConn"
			}
			return "rtspConn"
		}(),
		ID: c.uuid.String(),
	}

	c.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{
		Logger:              c,
		ExternalCmdPool:     c.externalCmdPool,
		RunOnConnect:        c.runOnConnect,
		RunOnConnectRestart: c.runOnConnectRestart,
		RunOnDisconnect:     c.runOnDisconnect,
		RTSPAddress:         c.rtspAddress,
		Desc:                desc,
	})
}

// Log implements logger.Writer.
func (c *conn) Log(level logger.Level, format string, args ...interface{}) {
	c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.rconn.NetConn().RemoteAddr()}, args...)...)
}

// Conn returns the RTSP connection.
func (c *conn) Conn() *gortsplib.ServerConn {
	return c.rconn
}

func (c *conn) remoteAddr() net.Addr {
	return c.rconn.NetConn().RemoteAddr()
}

func (c *conn) ip() net.IP {
	return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP
}

// onClose is called by rtspServer.
func (c *conn) onClose(err error) {
	c.Log(logger.Info, "closed: %v", err)

	c.onDisconnectHook()
}

// onRequest is called by rtspServer.
func (c *conn) onRequest(req *base.Request) {
	c.Log(logger.Debug, "[c->s] %v", req)
}

// OnResponse is called by rtspServer.
func (c *conn) OnResponse(res *base.Response) {
	c.Log(logger.Debug, "[s->c] %v", res)
}

// onDescribe is called by rtspServer.
func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
	if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
		return &base.Response{
			StatusCode: base.StatusBadRequest,
		}, nil, fmt.Errorf("invalid path")
	}
	ctx.Path = ctx.Path[1:]

	// CustomVerifyFunc prevents hashed credentials from working.
	// Use it only when strictly needed.
	var customVerifyFunc func(expectedUser, expectedPass string) bool
	if slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {
		customVerifyFunc = func(expectedUser, expectedPass string) bool {
			return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
		}
	}

	res := c.pathManager.Describe(defs.PathDescribeReq{
		AccessRequest: defs.PathAccessRequest{
			Name:             ctx.Path,
			Query:            ctx.Query,
			Proto:            auth.ProtocolRTSP,
			ID:               &c.uuid,
			Credentials:      rtsp.Credentials(ctx.Request),
			IP:               c.ip(),
			CustomVerifyFunc: customVerifyFunc,
		},
	})

	if res.Err != nil {
		var terr *auth.Error
		if errors.As(res.Err, &terr) {
			res, err2 := c.handleAuthError(terr)
			return res, nil, err2
		}

		var terr2 defs.PathNoStreamAvailableError
		if errors.As(res.Err, &terr2) {
			return &base.Response{
				StatusCode: base.StatusNotFound,
			}, nil, res.Err
		}

		return &base.Response{
			StatusCode: base.StatusBadRequest,
		}, nil, res.Err
	}

	if res.Redirect != "" {
		return &base.Response{
			StatusCode: base.StatusMovedPermanently,
			Header: base.Header{
				"Location": base.HeaderValue{absoluteURL(ctx.Request, res.Redirect)},
			},
		}, nil, nil
	}

	var stream *gortsplib.ServerStream
	if !c.isTLS {
		stream = res.Stream.RTSPStream(c.rserver)
	} else {
		stream = res.Stream.RTSPSStream(c.rserver)
	}

	return &base.Response{
		StatusCode: base.StatusOK,
	}, stream, nil
}

func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {
	if err.AskCredentials {
		return &base.Response{
			StatusCode: base.StatusUnauthorized,
		}, liberrors.ErrServerAuth{}
	}

	// wait some seconds to delay brute force attacks
	<-time.After(auth.PauseAfterError)

	return &base.Response{
		StatusCode: base.StatusUnauthorized,
	}, err
}

func (c *conn) apiItem() *defs.APIRTSPConn {
	stats := c.rconn.Stats()

	return &defs.APIRTSPConn{
		ID:            c.uuid,
		Created:       c.created,
		RemoteAddr:    c.remoteAddr().String(),
		BytesReceived: stats.BytesReceived,
		BytesSent:     stats.BytesSent,
		Session: func() *uuid.UUID {
			sx := c.parent.findSessionByRSessionUnsafe(c.rconn.Session())
			if sx != nil {
				return &sx.uuid
			}
			return nil
		}(),
		Tunnel: tunnelLabel(c.rconn.Transport().Tunnel),
	}
}
