package conf

import (
	"encoding/json"
	"fmt"
	"net"
	"net/url"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/bluenviron/gortsplib/v5/pkg/base"
	"github.com/bluenviron/mediamtx/internal/logger"
)

var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/\.~:]+$`)

// IsValidPathName checks whether the path name is valid.
func IsValidPathName(name string) error {
	if name == "" {
		return fmt.Errorf("cannot be empty")
	}

	if name[0] == '/' {
		return fmt.Errorf("can't begin with a slash")
	}

	if name[len(name)-1] == '/' {
		return fmt.Errorf("can't end with a slash")
	}

	if !rePathName.MatchString(name) {
		return fmt.Errorf("can contain only alphanumeric characters, underscore, dot, tilde, minus, slash, colon")
	}

	return nil
}

func checkSRTPassphrase(passphrase string) error {
	switch {
	case len(passphrase) < 10 || len(passphrase) > 79:
		return fmt.Errorf("must be between 10 and 79 characters")

	default:
		return nil
	}
}

func checkRedirect(v string) error {
	if strings.HasPrefix(v, "/") {
		err := IsValidPathName(v[1:])
		if err != nil {
			return fmt.Errorf("'%s': %w", v, err)
		}
	} else {
		_, err := base.ParseURL(v)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid RTSP URL", v)
		}
	}

	return nil
}

// FindPathConf returns the configuration corresponding to the given path name.
func FindPathConf(pathConfs map[string]*Path, name string) (*Path, []string, error) {
	// normal path
	if pathConf, ok := pathConfs[name]; ok {
		return pathConf, nil, nil
	}

	err := IsValidPathName(name)
	if err != nil {
		return nil, nil, fmt.Errorf("invalid path name: %w (%s)", err, name)
	}

	// gather and sort all regexp-based path configs
	var regexpPathConfs []*Path
	for _, pathConf := range pathConfs {
		if pathConf.Regexp != nil {
			regexpPathConfs = append(regexpPathConfs, pathConf)
		}
	}
	sort.Slice(regexpPathConfs, func(i, j int) bool {
		// keep all and all_others at the end
		if regexpPathConfs[i].Name == "all" || regexpPathConfs[i].Name == "all_others" {
			return false
		}
		if regexpPathConfs[j].Name == "all" || regexpPathConfs[j].Name == "all_others" {
			return true
		}
		return regexpPathConfs[i].Name < regexpPathConfs[j].Name
	})

	// check path against regexp-based path configs
	for _, pathConf := range regexpPathConfs {
		m := pathConf.Regexp.FindStringSubmatch(name)
		if m != nil {
			return pathConf, m, nil
		}
	}

	return nil, nil, fmt.Errorf("path '%s' is not configured", name)
}

// Path is a path configuration.
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
type Path struct {
	Regexp *regexp.Regexp `json:"-"`    // filled by Check()
	Name   string         `json:"name"` // filled by Check()

	// General
	Source                     string   `json:"source"`
	SourceFingerprint          string   `json:"sourceFingerprint"`
	SourceOnDemand             bool     `json:"sourceOnDemand"`
	SourceOnDemandStartTimeout Duration `json:"sourceOnDemandStartTimeout"`
	SourceOnDemandCloseAfter   Duration `json:"sourceOnDemandCloseAfter"`
	MaxReaders                 int      `json:"maxReaders"`
	SRTReadPassphrase          string   `json:"srtReadPassphrase"`
	Fallback                   string   `json:"fallback"`
	UseAbsoluteTimestamp       bool     `json:"useAbsoluteTimestamp"`

	// Record
	Record                bool         `json:"record"`
	Playback              *bool        `json:"playback,omitempty"` // deprecated
	RecordPath            string       `json:"recordPath"`
	RecordFormat          RecordFormat `json:"recordFormat"`
	RecordPartDuration    Duration     `json:"recordPartDuration"`
	RecordMaxPartSize     StringSize   `json:"recordMaxPartSize"`
	RecordSegmentDuration Duration     `json:"recordSegmentDuration"`
	RecordDeleteAfter     Duration     `json:"recordDeleteAfter"`

	// Authentication (deprecated)
	PublishUser *Credential `json:"publishUser,omitempty"` // deprecated
	PublishPass *Credential `json:"publishPass,omitempty"` // deprecated
	PublishIPs  *IPNetworks `json:"publishIPs,omitempty"`  // deprecated
	ReadUser    *Credential `json:"readUser,omitempty"`    // deprecated
	ReadPass    *Credential `json:"readPass,omitempty"`    // deprecated
	ReadIPs     *IPNetworks `json:"readIPs,omitempty"`     // deprecated

	// Publisher source
	OverridePublisher        bool   `json:"overridePublisher"`
	DisablePublisherOverride *bool  `json:"disablePublisherOverride,omitempty"` // deprecated
	SRTPublishPassphrase     string `json:"srtPublishPassphrase"`

	// RTSP source
	RTSPTransport         RTSPTransport  `json:"rtspTransport"`
	RTSPAnyPort           bool           `json:"rtspAnyPort"`
	SourceProtocol        *RTSPTransport `json:"sourceProtocol,omitempty"`      // deprecated
	SourceAnyPortEnable   *bool          `json:"sourceAnyPortEnable,omitempty"` // deprecated
	RTSPRangeType         RTSPRangeType  `json:"rtspRangeType"`
	RTSPRangeStart        string         `json:"rtspRangeStart"`
	RTSPUDPReadBufferSize uint           `json:"rtspUDPReadBufferSize"`

	// MPEG-TS source
	MPEGTSUDPReadBufferSize uint `json:"mpegtsUDPReadBufferSize"`

	// RTP source
	RTPSDP               string `json:"rtpSDP"`
	RTPUDPReadBufferSize uint   `json:"rtpUDPReadBufferSize"`

	// Redirect source
	SourceRedirect string `json:"sourceRedirect"`

	// Raspberry Pi Camera source
	RPICameraCamID                 uint      `json:"rpiCameraCamID"`
	RPICameraSecondary             bool      `json:"rpiCameraSecondary"`
	RPICameraWidth                 uint      `json:"rpiCameraWidth"`
	RPICameraHeight                uint      `json:"rpiCameraHeight"`
	RPICameraHFlip                 bool      `json:"rpiCameraHFlip"`
	RPICameraVFlip                 bool      `json:"rpiCameraVFlip"`
	RPICameraBrightness            float64   `json:"rpiCameraBrightness"`
	RPICameraContrast              float64   `json:"rpiCameraContrast"`
	RPICameraSaturation            float64   `json:"rpiCameraSaturation"`
	RPICameraSharpness             float64   `json:"rpiCameraSharpness"`
	RPICameraExposure              string    `json:"rpiCameraExposure"`
	RPICameraAWB                   string    `json:"rpiCameraAWB"`
	RPICameraAWBGains              []float64 `json:"rpiCameraAWBGains"`
	RPICameraDenoise               string    `json:"rpiCameraDenoise"`
	RPICameraShutter               uint      `json:"rpiCameraShutter"`
	RPICameraMetering              string    `json:"rpiCameraMetering"`
	RPICameraGain                  float64   `json:"rpiCameraGain"`
	RPICameraEV                    float64   `json:"rpiCameraEV"`
	RPICameraROI                   string    `json:"rpiCameraROI"`
	RPICameraHDR                   bool      `json:"rpiCameraHDR"`
	RPICameraTuningFile            string    `json:"rpiCameraTuningFile"`
	RPICameraMode                  string    `json:"rpiCameraMode"`
	RPICameraFPS                   float64   `json:"rpiCameraFPS"`
	RPICameraAfMode                string    `json:"rpiCameraAfMode"`
	RPICameraAfRange               string    `json:"rpiCameraAfRange"`
	RPICameraAfSpeed               string    `json:"rpiCameraAfSpeed"`
	RPICameraLensPosition          float64   `json:"rpiCameraLensPosition"`
	RPICameraAfWindow              string    `json:"rpiCameraAfWindow"`
	RPICameraFlickerPeriod         uint      `json:"rpiCameraFlickerPeriod"`
	RPICameraTextOverlayEnable     bool      `json:"rpiCameraTextOverlayEnable"`
	RPICameraTextOverlay           string    `json:"rpiCameraTextOverlay"`
	RPICameraCodec                 string    `json:"rpiCameraCodec"`
	RPICameraIDRPeriod             uint      `json:"rpiCameraIDRPeriod"`
	RPICameraBitrate               uint      `json:"rpiCameraBitrate"`
	RPICameraProfile               *string   `json:"rpiCameraProfile,omitempty"` // deprecated
	RPICameraLevel                 *string   `json:"rpiCameraLevel,omitempty"`   // deprecated
	RPICameraHardwareH264Profile   string    `json:"rpiCameraHardwareH264Profile"`
	RPICameraHardwareH264Level     string    `json:"rpiCameraHardwareH264Level"`
	RPICameraSoftwareH264Profile   string    `json:"rpiCameraSoftwareH264Profile"`
	RPICameraSoftwareH264Level     string    `json:"rpiCameraSoftwareH264Level"`
	RPICameraJPEGQuality           *uint     `json:"rpiCameraJPEGQuality,omitempty"` // deprecated
	RPICameraMJPEGQuality          uint      `json:"rpiCameraMJPEGQuality"`
	RPICameraPrimaryName           string    `json:"-"` // filled by Check()
	RPICameraSecondaryWidth        uint      `json:"-"` // filled by Check()
	RPICameraSecondaryHeight       uint      `json:"-"` // filled by Check()
	RPICameraSecondaryFPS          float64   `json:"-"` // filled by Check()
	RPICameraSecondaryMJPEGQuality uint      `json:"-"` // filled by Check()

	// Hooks
	RunOnInit                  string   `json:"runOnInit"`
	RunOnInitRestart           bool     `json:"runOnInitRestart"`
	RunOnDemand                string   `json:"runOnDemand"`
	RunOnDemandRestart         bool     `json:"runOnDemandRestart"`
	RunOnDemandStartTimeout    Duration `json:"runOnDemandStartTimeout"`
	RunOnDemandCloseAfter      Duration `json:"runOnDemandCloseAfter"`
	RunOnUnDemand              string   `json:"runOnUnDemand"`
	RunOnReady                 string   `json:"runOnReady"`
	RunOnReadyRestart          bool     `json:"runOnReadyRestart"`
	RunOnNotReady              string   `json:"runOnNotReady"`
	RunOnRead                  string   `json:"runOnRead"`
	RunOnReadRestart           bool     `json:"runOnReadRestart"`
	RunOnUnread                string   `json:"runOnUnread"`
	RunOnRecordSegmentCreate   string   `json:"runOnRecordSegmentCreate"`
	RunOnRecordSegmentComplete string   `json:"runOnRecordSegmentComplete"`
}

func (pconf *Path) setDefaults() {
	// General
	pconf.Source = "publisher"
	pconf.SourceOnDemandStartTimeout = 10 * Duration(time.Second)
	pconf.SourceOnDemandCloseAfter = 10 * Duration(time.Second)

	// Record
	pconf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
	pconf.RecordFormat = RecordFormatFMP4
	pconf.RecordPartDuration = Duration(1 * time.Second)
	pconf.RecordMaxPartSize = 50 * 1024 * 1024
	pconf.RecordSegmentDuration = 3600 * Duration(time.Second)
	pconf.RecordDeleteAfter = 24 * 3600 * Duration(time.Second)

	// Publisher source
	pconf.OverridePublisher = true

	// Raspberry Pi Camera source
	pconf.RPICameraWidth = 1920
	pconf.RPICameraHeight = 1080
	pconf.RPICameraContrast = 1
	pconf.RPICameraSaturation = 1
	pconf.RPICameraSharpness = 1
	pconf.RPICameraExposure = "normal"
	pconf.RPICameraAWB = "auto"
	pconf.RPICameraAWBGains = []float64{0, 0}
	pconf.RPICameraDenoise = "off"
	pconf.RPICameraMetering = "centre"
	pconf.RPICameraFPS = 30
	pconf.RPICameraAfMode = "continuous"
	pconf.RPICameraAfRange = "normal"
	pconf.RPICameraAfSpeed = "normal"
	pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX"
	pconf.RPICameraCodec = "auto"
	pconf.RPICameraIDRPeriod = 60
	pconf.RPICameraBitrate = 5000000
	pconf.RPICameraHardwareH264Profile = "main"
	pconf.RPICameraHardwareH264Level = "4.1"
	pconf.RPICameraSoftwareH264Profile = "baseline"
	pconf.RPICameraSoftwareH264Level = "4.1"
	pconf.RPICameraMJPEGQuality = 60

	// Hooks
	pconf.RunOnDemandStartTimeout = 10 * Duration(time.Second)
	pconf.RunOnDemandCloseAfter = 10 * Duration(time.Second)
}

func newPath(defaults *Path, partial *OptionalPath) *Path {
	pconf := &Path{}
	copyStructFields(pconf, defaults)
	copyStructFields(pconf, partial.Values)
	return pconf
}

// Clone clones the configuration.
func (pconf Path) Clone() *Path {
	enc, err := json.Marshal(pconf)
	if err != nil {
		panic(err)
	}

	var dest Path
	err = json.Unmarshal(enc, &dest)
	if err != nil {
		panic(err)
	}

	dest.Regexp = pconf.Regexp
	dest.RPICameraPrimaryName = pconf.RPICameraPrimaryName
	dest.RPICameraSecondaryWidth = pconf.RPICameraSecondaryWidth
	dest.RPICameraSecondaryHeight = pconf.RPICameraSecondaryHeight
	dest.RPICameraSecondaryFPS = pconf.RPICameraSecondaryFPS
	dest.RPICameraSecondaryMJPEGQuality = pconf.RPICameraSecondaryMJPEGQuality

	return &dest
}

func (pconf *Path) validate(
	conf *Conf,
	name string,
	deprecatedCredentialsMode bool,
	l logger.Writer,
) error {
	pconf.Name = name

	switch {
	case name == "all_others", name == "all":
		pconf.Regexp = regexp.MustCompile("^.*$")

	case name == "" || name[0] != '~': // normal path
		err := IsValidPathName(name)
		if err != nil {
			return fmt.Errorf("invalid path name '%s': %w", name, err)
		}

	default: // regular expression-based path
		regexp, err := regexp.Compile(name[1:])
		if err != nil {
			return fmt.Errorf("invalid regular expression: %s", name[1:])
		}
		pconf.Regexp = regexp
	}

	// common configuration errors

	if pconf.Source != "publisher" && pconf.Source != "redirect" &&
		pconf.Regexp != nil && !pconf.SourceOnDemand {
		return fmt.Errorf("a path with a regular expression (or path 'all') and a static source" +
			" must have 'sourceOnDemand' set to true")
	}

	if pconf.SRTPublishPassphrase != "" && pconf.Source != "publisher" {
		return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'")
	}

	if pconf.SourceOnDemand && pconf.Source == "publisher" {
		return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'")
	}

	if pconf.Source != "redirect" && pconf.SourceRedirect != "" {
		return fmt.Errorf("'sourceRedirect' is useless when source is not 'redirect'")
	}

	// source-dependent settings

	switch {
	case pconf.Source == "publisher":
		if pconf.DisablePublisherOverride != nil {
			l.Log(logger.Warn, "parameter 'disablePublisherOverride' is deprecated "+
				"and has been replaced with 'overridePublisher'")
			pconf.OverridePublisher = !*pconf.DisablePublisherOverride
		}

		if pconf.SRTPublishPassphrase != "" {
			err := checkSRTPassphrase(pconf.SRTPublishPassphrase)
			if err != nil {
				return fmt.Errorf("invalid 'srtPublishPassphrase': %w", err)
			}
		}

	case strings.HasPrefix(pconf.Source, "rtsp://") ||
		strings.HasPrefix(pconf.Source, "rtsps://") ||
		strings.HasPrefix(pconf.Source, "rtsp+http://") ||
		strings.HasPrefix(pconf.Source, "rtsps+http://") ||
		strings.HasPrefix(pconf.Source, "rtsp+ws://") ||
		strings.HasPrefix(pconf.Source, "rtsps+ws://"):
		_, err := url.Parse(pconf.Source)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
		}

		if pconf.SourceProtocol != nil {
			l.Log(logger.Warn, "parameter 'sourceProtocol' is deprecated and has been replaced with 'rtspTransport'")
			pconf.RTSPTransport = *pconf.SourceProtocol
		}

		if pconf.SourceAnyPortEnable != nil {
			l.Log(logger.Warn, "parameter 'sourceAnyPortEnable' is deprecated and has been replaced with 'rtspAnyPort'")
			pconf.RTSPAnyPort = *pconf.SourceAnyPortEnable
		}

	case strings.HasPrefix(pconf.Source, "rtmp://") ||
		strings.HasPrefix(pconf.Source, "rtmps://"):
		u, err := url.Parse(pconf.Source)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
		}

		if u.User != nil {
			pass, _ := u.User.Password()
			user := u.User.Username()
			if user != "" && pass == "" ||
				user == "" && pass != "" {
				return fmt.Errorf("username and password must be both provided")
			}
		}

	case strings.HasPrefix(pconf.Source, "http://") ||
		strings.HasPrefix(pconf.Source, "https://"):
		u, err := url.Parse(pconf.Source)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
		}

		if u.User != nil {
			pass, _ := u.User.Password()
			user := u.User.Username()
			if user != "" && pass == "" ||
				user == "" && pass != "" {
				return fmt.Errorf("username and password must be both provided")
			}
		}

	case strings.HasPrefix(pconf.Source, "udp://"):
		_, _, err := net.SplitHostPort(pconf.Source[len("udp://"):])
		if err != nil {
			return fmt.Errorf("'%s' is not a valid UDP+MPEGTS URL", pconf.Source)
		}

	case strings.HasPrefix(pconf.Source, "udp+mpegts://"):
		_, _, err := net.SplitHostPort(pconf.Source[len("udp+mpegts://"):])
		if err != nil {
			return fmt.Errorf("'%s' is not a valid UDP+MPEGTS URL", pconf.Source)
		}

	case strings.HasPrefix(pconf.Source, "unix+mpegts://"):

	case strings.HasPrefix(pconf.Source, "udp+rtp://"):
		_, _, err := net.SplitHostPort(pconf.Source[len("udp+rtp://"):])
		if err != nil {
			return fmt.Errorf("'%s' is not a valid UDP+RTP URL", pconf.Source)
		}

		if pconf.RTPSDP == "" {
			return fmt.Errorf("`rtpSDP` was not provided")
		}

	case strings.HasPrefix(pconf.Source, "unix+rtp://"):
		if pconf.RTPSDP == "" {
			return fmt.Errorf("`rtpSDP` was not provided")
		}

	case strings.HasPrefix(pconf.Source, "srt://"):
		_, err := url.Parse(pconf.Source)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
		}

	case strings.HasPrefix(pconf.Source, "whep://") ||
		strings.HasPrefix(pconf.Source, "wheps://"):
		_, err := url.Parse(pconf.Source)
		if err != nil {
			return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
		}

	case pconf.Source == "redirect":
		if pconf.SourceRedirect == "" {
			return fmt.Errorf("source redirect must be filled")
		}

		err := checkRedirect(pconf.SourceRedirect)
		if err != nil {
			return err
		}

	case pconf.Source == "rpiCamera":

		if pconf.RPICameraWidth == 0 {
			return fmt.Errorf("invalid 'rpiCameraWidth' value")
		}

		if pconf.RPICameraHeight == 0 {
			return fmt.Errorf("invalid 'rpiCameraHeight' value")
		}

		switch pconf.RPICameraExposure {
		case "normal", "short", "long", "custom":
		default:
			return fmt.Errorf("invalid 'rpiCameraExposure' value")
		}

		switch pconf.RPICameraAWB {
		case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom":
		default:
			return fmt.Errorf("invalid 'rpiCameraAWB' value")
		}

		if len(pconf.RPICameraAWBGains) != 2 {
			return fmt.Errorf("invalid 'rpiCameraAWBGains' value")
		}

		switch pconf.RPICameraDenoise {
		case "off", "cdn_off", "cdn_fast", "cdn_hq":
		default:
			return fmt.Errorf("invalid 'rpiCameraDenoise' value")
		}

		switch pconf.RPICameraMetering {
		case "centre", "spot", "matrix", "custom":
		default:
			return fmt.Errorf("invalid 'rpiCameraMetering' value")
		}

		switch pconf.RPICameraAfMode {
		case "auto", "manual", "continuous":
		default:
			return fmt.Errorf("invalid 'rpiCameraAfMode' value")
		}

		switch pconf.RPICameraAfRange {
		case "normal", "macro", "full":
		default:
			return fmt.Errorf("invalid 'rpiCameraAfRange' value")
		}

		switch pconf.RPICameraAfSpeed {
		case "normal", "fast":
		default:
			return fmt.Errorf("invalid 'rpiCameraAfSpeed' value")
		}

		if pconf.RPICameraProfile != nil {
			l.Log(logger.Warn, "parameter 'rpiCameraProfile' is deprecated"+
				" and has been replaced with 'rpiCameraHardwareH264Profile'")
			pconf.RPICameraHardwareH264Profile = *pconf.RPICameraProfile
		}

		if pconf.RPICameraLevel != nil {
			l.Log(logger.Warn, "parameter 'rpiCameraLevel' is deprecated"+
				" and has been replaced with 'rpiCameraHardwareH264Level'")
			pconf.RPICameraHardwareH264Level = *pconf.RPICameraLevel
		}

		switch pconf.RPICameraHardwareH264Profile {
		case "baseline", "main", "high":
		default:
			return fmt.Errorf("invalid 'rpiCameraHardwareH264Profile' value")
		}

		switch pconf.RPICameraHardwareH264Level {
		case "4.0", "4.1", "4.2":
		default:
			return fmt.Errorf("invalid 'rpiCameraHardwareH264Level' value")
		}

		switch pconf.RPICameraSoftwareH264Profile {
		case "baseline", "main", "high":
		default:
			return fmt.Errorf("invalid 'rpiCameraSoftwareH264Profile' value")
		}

		switch pconf.RPICameraSoftwareH264Level {
		case "4.0", "4.1", "4.2":
		default:
			return fmt.Errorf("invalid 'rpiCameraSoftwareH264Level' value")
		}

		if pconf.RPICameraJPEGQuality != nil {
			l.Log(logger.Warn, "parameter 'rpiCameraJPEGQuality' is deprecated"+
				" and has been replaced with 'rpiCameraMJPEGQuality'")
			pconf.RPICameraMJPEGQuality = *pconf.RPICameraJPEGQuality
		}

		if !pconf.RPICameraSecondary {
			switch pconf.RPICameraCodec {
			case "auto", "hardwareH264", "softwareH264":
			default:
				return fmt.Errorf("supported codecs for a primary RPI Camera stream are auto, hardwareH264, softwareH264")
			}

			for otherName, otherPath := range conf.Paths {
				if otherPath != pconf &&
					otherPath != nil &&
					otherPath.Source == "rpiCamera" &&
					otherPath.RPICameraCamID == pconf.RPICameraCamID &&
					!otherPath.RPICameraSecondary {
					return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'",
						pconf.RPICameraCamID, name, otherName)
				}
			}
		} else {
			switch pconf.RPICameraCodec {
			case "auto", "mjpeg":
			default:
				return fmt.Errorf("supported codecs for a secondary RPI Camera stream are auto, mjpeg")
			}

			var primaryName string
			var primary *Path

			for otherPathName, otherPath := range conf.Paths {
				if otherPath != pconf &&
					otherPath != nil &&
					otherPath.Source == "rpiCamera" &&
					otherPath.RPICameraCamID == pconf.RPICameraCamID &&
					!otherPath.RPICameraSecondary {
					primaryName = otherPathName
					primary = otherPath
					break
				}
			}

			if primary == nil {
				return fmt.Errorf("cannot find a primary RPI Camera stream to associate with the secondary stream")
			}

			if primary.RPICameraSecondaryWidth != 0 {
				return fmt.Errorf("a primary RPI Camera stream is associated with multiple secondary streams")
			}

			pconf.RPICameraPrimaryName = primaryName
			primary.RPICameraSecondaryWidth = pconf.RPICameraWidth
			primary.RPICameraSecondaryHeight = pconf.RPICameraHeight
			primary.RPICameraSecondaryFPS = pconf.RPICameraFPS
			primary.RPICameraSecondaryMJPEGQuality = pconf.RPICameraMJPEGQuality
		}

	default:
		return fmt.Errorf("invalid source: '%s'", pconf.Source)
	}

	if pconf.SRTReadPassphrase != "" {
		err := checkSRTPassphrase(pconf.SRTReadPassphrase)
		if err != nil {
			return fmt.Errorf("invalid 'readRTPassphrase': %w", err)
		}
	}

	if pconf.Fallback != "" {
		err := checkRedirect(pconf.Fallback)
		if err != nil {
			return err
		}
	}

	// Record

	if pconf.Playback != nil {
		l.Log(logger.Warn, "parameter 'playback' is deprecated and has no effect")
	}

	if !strings.Contains(pconf.RecordPath, "%path") {
		return fmt.Errorf("'recordPath' must contain %%path")
	}

	if !strings.Contains(pconf.RecordPath, "%s") &&
		(!strings.Contains(pconf.RecordPath, "%Y") ||
			!strings.Contains(pconf.RecordPath, "%m") ||
			!strings.Contains(pconf.RecordPath, "%d") ||
			!strings.Contains(pconf.RecordPath, "%H") ||
			!strings.Contains(pconf.RecordPath, "%M") ||
			!strings.Contains(pconf.RecordPath, "%S")) {
		return fmt.Errorf("'recordPath' must contain either %%s or %%Y %%m %%d %%H %%M %%S")
	}

	if conf.Playback && !strings.Contains(pconf.RecordPath, "%f") {
		return fmt.Errorf("'recordPath' must contain %%f")
	}

	if pconf.RecordSegmentDuration > Duration(24*time.Hour) { // avoid overflowing DurationV0 of mvhd
		return fmt.Errorf("maximum segment duration is 1 day")
	}

	if pconf.RecordDeleteAfter != 0 && pconf.RecordDeleteAfter < pconf.RecordSegmentDuration {
		return fmt.Errorf("'recordDeleteAfter' cannot be lower than 'recordSegmentDuration'")
	}

	// Authentication (deprecated)

	if deprecatedCredentialsMode {
		func() {
			var user Credential = "any"
			if pconf.PublishUser != nil && *pconf.PublishUser != "" {
				user = *pconf.PublishUser
			}

			var pass Credential
			if pconf.PublishPass != nil && *pconf.PublishPass != "" {
				pass = *pconf.PublishPass
			}

			ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
			if pconf.PublishIPs != nil && len(*pconf.PublishIPs) != 0 {
				ips = *pconf.PublishIPs
			}

			pathName := name
			if name == "all_others" || name == "all" {
				pathName = "~^.*$"
			}

			conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{
				User: user,
				Pass: pass,
				IPs:  ips,
				Permissions: []AuthInternalUserPermission{{
					Action: AuthActionPublish,
					Path:   pathName,
				}},
			})
		}()

		func() {
			var user Credential = "any"
			if pconf.ReadUser != nil && *pconf.ReadUser != "" {
				user = *pconf.ReadUser
			}

			var pass Credential
			if pconf.ReadPass != nil && *pconf.ReadPass != "" {
				pass = *pconf.ReadPass
			}

			ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
			if pconf.ReadIPs != nil && len(*pconf.ReadIPs) != 0 {
				ips = *pconf.ReadIPs
			}

			pathName := name
			if name == "all_others" || name == "all" {
				pathName = "~^.*$"
			}

			conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{
				User: user,
				Pass: pass,
				IPs:  ips,
				Permissions: []AuthInternalUserPermission{{
					Action: AuthActionRead,
					Path:   pathName,
				}},
			})
		}()
	}

	// Hooks

	if pconf.RunOnInit != "" && pconf.Regexp != nil {
		return fmt.Errorf("a path with a regular expression (or path 'all')" +
			" does not support option 'runOnInit'; use another path")
	}
	if (pconf.RunOnDemand != "" || pconf.RunOnUnDemand != "") && pconf.Source != "publisher" {
		return fmt.Errorf("'runOnDemand' and 'runOnUnDemand' can be used only when source is 'publisher'")
	}

	return nil
}

// Equal checks whether two Paths are equal.
func (pconf *Path) Equal(other *Path) bool {
	return reflect.DeepEqual(pconf, other)
}

// HasStaticSource checks whether the path has a static source.
func (pconf Path) HasStaticSource() bool {
	return pconf.Source != "publisher" && pconf.Source != "redirect"
}

// HasOnDemandStaticSource checks whether the path has a on demand static source.
func (pconf Path) HasOnDemandStaticSource() bool {
	return pconf.HasStaticSource() && pconf.SourceOnDemand
}

// HasOnDemandPublisher checks whether the path has a on-demand publisher.
func (pconf Path) HasOnDemandPublisher() bool {
	return pconf.RunOnDemand != ""
}
