import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { getProjectDataDir } from '../lib/project-data-dir'

const LATENCY_ROOT_SEGMENTS = ['data', 'latency']
const RECORDING_FILE_NAME = 'recording.wav'
const METADATA_FILE_NAME = 'metadata.json'

export type LatencyRecordingMetadata = {
	conversationId: string
	sourceId: string
	fileName: string
	mimeType: string
	size: number
	uploadedAt: string
	alignmentOffsetMs: number
}

export type LatencyRecordingRecord = LatencyRecordingMetadata & {
	audioPath: string
}

function normalizeSourceId(sourceId: string | null | undefined): string {
	const trimmed = String(sourceId ?? '').trim()
	return trimmed || 'default'
}

function makeSafePathSegment(raw: string, fallback: string): string {
	const trimmed = String(raw ?? '').trim()
	const value = trimmed || fallback
	const slug =
		value
			.slice(0, 80)
			.replace(/[^a-zA-Z0-9._-]+/g, '_')
			.replace(/^_+|_+$/g, '') || fallback
	const hash = createHash('sha1').update(value).digest('hex').slice(0, 12)
	return `${slug}-${hash}`
}

async function resolveConversationDir(
	conversationId: string,
	sourceId: string | null | undefined
): Promise<string> {
	const normalizedSourceId = normalizeSourceId(sourceId)

	return getProjectDataDir(
		...LATENCY_ROOT_SEGMENTS.slice(1),
		makeSafePathSegment(normalizedSourceId, 'source'),
		makeSafePathSegment(conversationId, 'conversation')
	)
}

async function readMetadataFile(filePath: string): Promise<LatencyRecordingMetadata | null> {
	try {
		const raw = await fs.readFile(filePath, 'utf-8')
		const parsed = JSON.parse(raw) as Partial<LatencyRecordingMetadata>
		if (
			typeof parsed?.conversationId !== 'string' ||
			typeof parsed?.sourceId !== 'string' ||
			typeof parsed?.fileName !== 'string' ||
			typeof parsed?.mimeType !== 'string' ||
			typeof parsed?.size !== 'number' ||
			typeof parsed?.uploadedAt !== 'string' ||
			typeof parsed?.alignmentOffsetMs !== 'number'
		) {
			return null
		}
		return parsed as LatencyRecordingMetadata
	} catch (error) {
		if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
		console.error('Failed to read latency recording metadata:', filePath, error)
		return null
	}
}

async function writeMetadataFile(filePath: string, metadata: LatencyRecordingMetadata) {
	await fs.mkdir(path.dirname(filePath), { recursive: true })
	await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf-8')
}

export function isWavBytes(bytes: Uint8Array): boolean {
	if (!(bytes instanceof Uint8Array) || bytes.byteLength < 12) return false

	const riff =
		bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46
	const wave =
		bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45

	return riff && wave
}

export async function getLatencyRecording(
	conversationId: string,
	sourceId: string | null | undefined
): Promise<LatencyRecordingRecord | null> {
	const normalizedSourceId = normalizeSourceId(sourceId)
	const dirPath = await resolveConversationDir(conversationId, normalizedSourceId)
	const audioPath = path.join(dirPath, RECORDING_FILE_NAME)
	const metadataPath = path.join(dirPath, METADATA_FILE_NAME)

	const [metadata, audioStat] = await Promise.all([
		readMetadataFile(metadataPath),
		fs.stat(audioPath).catch((error: NodeJS.ErrnoException) => {
			if (error.code === 'ENOENT') return null
			throw error
		})
	])

	if (!metadata || !audioStat?.isFile()) return null

	return {
		...metadata,
		audioPath,
		size: Number.isFinite(metadata.size) && metadata.size > 0 ? metadata.size : audioStat.size
	}
}

export async function saveLatencyRecording(input: {
	conversationId: string
	sourceId: string | null | undefined
	fileName: string
	mimeType: string
	bytes: Uint8Array
}) {
	const normalizedSourceId = normalizeSourceId(input.sourceId)
	const dirPath = await resolveConversationDir(input.conversationId, normalizedSourceId)
	const audioPath = path.join(dirPath, RECORDING_FILE_NAME)
	const metadataPath = path.join(dirPath, METADATA_FILE_NAME)

	await fs.mkdir(dirPath, { recursive: true })
	await fs.writeFile(audioPath, input.bytes)

	const metadata: LatencyRecordingMetadata = {
		conversationId: input.conversationId,
		sourceId: normalizedSourceId,
		fileName: input.fileName.trim() || RECORDING_FILE_NAME,
		mimeType: input.mimeType.trim() || 'audio/wav',
		size: input.bytes.byteLength,
		uploadedAt: new Date().toISOString(),
		alignmentOffsetMs: 0
	}

	await writeMetadataFile(metadataPath, metadata)

	return {
		...metadata,
		audioPath
	} satisfies LatencyRecordingRecord
}

export async function updateLatencyRecordingAlignment(input: {
	conversationId: string
	sourceId: string | null | undefined
	alignmentOffsetMs: number
}) {
	const existing = await getLatencyRecording(input.conversationId, input.sourceId)
	if (!existing) return null

	const dirPath = await resolveConversationDir(input.conversationId, existing.sourceId)
	const metadataPath = path.join(dirPath, METADATA_FILE_NAME)
	const metadata: LatencyRecordingMetadata = {
		conversationId: existing.conversationId,
		sourceId: existing.sourceId,
		fileName: existing.fileName,
		mimeType: existing.mimeType,
		size: existing.size,
		uploadedAt: existing.uploadedAt,
		alignmentOffsetMs: input.alignmentOffsetMs
	}

	await writeMetadataFile(metadataPath, metadata)

	return {
		...metadata,
		audioPath: existing.audioPath
	} satisfies LatencyRecordingRecord
}
