package metadata

import (
	"encoding/binary"
	"errors"
	"fmt"
	"os"
)

// errInvalidFLACSignature is returned when the file does not contain
// a valid FLAC stream signature ("fLaC") at the expected position.
var errInvalidFLACSignature = errors.New(
	"invalid FLAC signature",
)

// errInvalidStreamInfo is returned when the first metadata block is
// not a StreamInfo block or has an unexpected length.
var errInvalidStreamInfo = errors.New(
	"invalid StreamInfo metadata block",
)

// errZeroSampleRate is returned when the StreamInfo block reports a
// sample rate of zero, which would cause a division by zero.
var errZeroSampleRate = errors.New(
	"FLAC StreamInfo sample rate is zero",
)

// flacSignatureBytes is the four-byte marker that begins every FLAC
// stream.
var flacSignatureBytes = [4]byte{'f', 'L', 'a', 'C'}

// streamInfoLength is the fixed size of a FLAC StreamInfo body in
// bytes.
const streamInfoLength = 34

// streamInfoBlockType is the metadata block type for StreamInfo.
const streamInfoBlockType = 0

// getFlacDuration computes the duration of a FLAC file in
// milliseconds by reading only the StreamInfo metadata block header.
// It also extracts sample rate, bit depth, and channel count from
// the same header.  It handles an optional prepended ID3v2 tag by
// seeking past it.
//
// This replaces the previous beep/mewkiz-flac decode path which has
// a bug in its ID3v2 skip logic (bufio over bufseekio causes a
// position overshoot).
//
// The file position is undefined after this call.
//
//nolint:mnd // byte offsets and bit shifts from the FLAC spec.
func getFlacDuration(
	f *os.File,
) (int64, *AudioProperties, error) {
	audioStart, err := skipID3v2(f)
	if err != nil {
		return 0, nil, fmt.Errorf("skipping ID3v2: %w", err)
	}

	// Read the 4-byte FLAC signature.
	var sig [4]byte

	if _, err := f.ReadAt(sig[:], audioStart); err != nil {
		return 0, nil, fmt.Errorf(
			"reading FLAC signature: %w", err,
		)
	}

	if sig != flacSignatureBytes {
		return 0, nil, fmt.Errorf(
			"%w: expected %q, got %q",
			errInvalidFLACSignature, flacSignatureBytes, sig,
		)
	}

	// Read the metadata block header (4 bytes) immediately after
	// the signature.
	var mbh [4]byte

	if _, err := f.ReadAt(
		mbh[:], audioStart+4,
	); err != nil {
		return 0, nil, fmt.Errorf(
			"reading metadata block header: %w", err,
		)
	}

	blockType := mbh[0] & 0x7F

	blockLen := int64(mbh[1])<<16 |
		int64(mbh[2])<<8 |
		int64(mbh[3])

	if blockType != streamInfoBlockType ||
		blockLen != streamInfoLength {
		return 0, nil, fmt.Errorf(
			"%w: type=%d, length=%d",
			errInvalidStreamInfo, blockType, blockLen,
		)
	}

	// Read the 34-byte StreamInfo body.
	var si [streamInfoLength]byte

	if _, err := f.ReadAt(
		si[:], audioStart+8,
	); err != nil {
		return 0, nil, fmt.Errorf(
			"reading StreamInfo block: %w", err,
		)
	}

	sampleRate, totalSamples, channels, bitDepth := parseFlacStreamInfo(si)

	if sampleRate == 0 {
		return 0, nil, errZeroSampleRate
	}

	durationMS := int64(totalSamples) * 1000 /
		int64(sampleRate)

	props := &AudioProperties{
		SampleRate: int(sampleRate),
		BitDepth:   int(bitDepth),
		Channels:   int(channels),
	}

	return durationMS, props, nil
}

// parseFlacStreamInfo extracts key fields from a 34-byte FLAC
// StreamInfo body.
//
// StreamInfo layout (bytes 10-17 contain the fields we need):
//
//	bits  0-19:  sample rate in Hz     (20 bits)
//	bits 20-22:  number of channels -1 (3 bits)
//	bits 23-27:  bits per sample -1    (5 bits)
//	bits 28-63:  total samples         (36 bits)
//
//nolint:mnd // bit offsets from the FLAC spec.
func parseFlacStreamInfo(
	si [streamInfoLength]byte,
) (sampleRate uint32, totalSamples uint64, channels uint32, bitDepth uint32) {
	// Bytes 10-13 packed as big-endian uint32 contain sample rate
	// in the upper 20 bits, channels in bits 9-11, and bits per
	// sample in bits 4-8.
	packed := binary.BigEndian.Uint32(si[10:14])
	sampleRate = packed >> 12
	channels = (packed>>9)&0x07 + 1
	bitDepth = (packed>>4)&0x1F + 1

	// Total samples: 4 low bits of byte 13, then bytes 14-17.
	totalSamples = uint64(si[13]&0x0F)<<32 |
		uint64(si[14])<<24 |
		uint64(si[15])<<16 |
		uint64(si[16])<<8 |
		uint64(si[17])

	return sampleRate, totalSamples, channels, bitDepth
}
