package sqlite

import (
	"database/sql"

	"git.vakhrushev.me/av/transcriber/internal/entity"
)

// Отображение аудиозаписи в строку базы и обратно живёт одним местом.
//
// Работает оно **по имени колонки**: именованные параметры запроса и место
// назначения, найденное по имени. Причина в самой сущности — у аудиозаписи поля
// одного типа идут длинным непрерывным рядом, и ссылки на файл, на структуру
// реплик, на два вида текста и на попытку распознавания стоят в нём подряд.
// Позиционный список дал бы сдвиг на одно поле, который компилируется молча и
// кладёт идентификатор файла в колонку текста. По имени такого сдвига не
// существует вовсе: лишнее имя или недостающее — отказ запроса, а не тихая
// подмена значения.
//
// Инвариант проекта о колонках записи эта форма не снимает: колонку по-прежнему
// можно забыть в отображении или в шаге схемы, и сверку держат правила
// `internal/archrules`.

// writeOwnedByPipeline — колонки, которыми распоряжается конвейер.
//
// Разрез нужен потому, что шаг держит запись снимком с момента захвата и до
// своего сохранения, а это часы. Всё, что владелец правил за это время,
// безусловная запись снимка стёрла бы молча: ни строки в журнале, ни отказа
// тому, кто правил. Владелец, заголовок, краткое описание, имя файла
// отправителя, длительность, размер и темы не трогаются вовсе.
func writeOwnedByPipeline(r *entity.AudioRecord) map[string]any {
	return map[string]any{
		"state":              r.State,
		"state_entered_at":   formatTime(r.StateEnteredAt),
		"halted_at":          timeValue(r.HaltedAt),
		"halt_reason":        stringValue(r.HaltReason),
		"error_text":         stringValue(r.ErrorText),
		"acquisition_id":     stringValue(r.AcquisitionID),
		"acquire_expires_at": timeValue(r.AcquireExpiresAt),
		"delay_time":         timeValue(r.DelayTime),
		"attempts":           r.Attempts,
		"original_file_id":   stringValue(r.OriginalFileID),
		"normalized_file_id": stringValue(r.NormalizedFileID),
		"transcript_text_id": stringValue(r.TranscriptTextID),
		"literary_text_id":   stringValue(r.LiteraryTextID),
		"structure_id":       stringValue(r.StructureID),
		"recognition_id":     stringValue(r.RecognitionID),
		"updated_at":         formatTime(r.UpdatedAt),
	}
}

// writeRecord — запись целиком: это заведение, и спорить за поля здесь не с кем.
//
// Колонки заведения перечислены той же формой, что и колонки конвейера, — картой
// «имя колонки в значение»: сверка колонок в `internal/archrules` читает именно
// её, и колонка, положенная присваиванием мимо карты, выпала бы из-под правила
// молча.
func writeRecord(r *entity.AudioRecord) map[string]any {
	values := writeOwnedByPipeline(r)

	own := map[string]any{
		"id": r.Id,
		// Владелец кладётся только здесь, при заведении. В перечне конвейера его
		// нет намеренно: конвейер владельца не назначает и не меняет, а снимок
		// шага, записанный поверх, стёр бы его молча.
		"owner_id": r.OwnerID,
		"title":    stringValue(r.Title),
		"brief":    stringValue(r.Brief),
		// Имя файла отправителя, длительность и размер кладёт приём и только он:
		// это снимок принятого, и конвейер его не пересчитывает.
		"original_filename": stringValue(r.OriginalFilename),
		"duration_ms":       numberValue(r.DurationMs),
		"size_bytes":        numberValue(r.SizeBytes),
		"created_at":        formatTime(r.CreatedAt),
	}
	for name, value := range own {
		values[name] = value
	}

	return values
}

// recordRow — сырые значения одной строки аудиозаписи.
type recordRow struct {
	id               string
	ownerID          string
	title            sql.NullString
	brief            sql.NullString
	originalFilename sql.NullString
	durationMs       int64
	sizeBytes        int64
	state            string
	stateEnteredAt   string
	haltedAt         sql.NullString
	haltReason       sql.NullString
	errorText        sql.NullString
	acquisitionID    sql.NullString
	acquireExpiresAt sql.NullString
	delayTime        sql.NullString
	attempts         int
	originalFileID   sql.NullString
	normalizedFileID sql.NullString
	transcriptTextID sql.NullString
	literaryTextID   sql.NullString
	structureID      sql.NullString
	recognitionID    sql.NullString
	createdAt        string
	updatedAt        string
}

// readRecordColumns — куда кладётся каждая колонка при чтении.
//
// Перечень колонок выборки собирается из этой же карты, поэтому расхождению
// между тем, что спрошено, и тем, куда оно ляжет, взяться неоткуда.
func readRecordColumns(row *recordRow) map[string]any {
	return map[string]any{
		"id":                 &row.id,
		"owner_id":           &row.ownerID,
		"title":              &row.title,
		"brief":              &row.brief,
		"original_filename":  &row.originalFilename,
		"duration_ms":        &row.durationMs,
		"size_bytes":         &row.sizeBytes,
		"state":              &row.state,
		"state_entered_at":   &row.stateEnteredAt,
		"halted_at":          &row.haltedAt,
		"halt_reason":        &row.haltReason,
		"error_text":         &row.errorText,
		"acquisition_id":     &row.acquisitionID,
		"acquire_expires_at": &row.acquireExpiresAt,
		"delay_time":         &row.delayTime,
		"attempts":           &row.attempts,
		"original_file_id":   &row.originalFileID,
		"normalized_file_id": &row.normalizedFileID,
		"transcript_text_id": &row.transcriptTextID,
		"literary_text_id":   &row.literaryTextID,
		"structure_id":       &row.structureID,
		"recognition_id":     &row.recognitionID,
		"created_at":         &row.createdAt,
		"updated_at":         &row.updatedAt,
	}
}

// rowToAudioRecord собирает доменную запись из прочитанной строки.
func rowToAudioRecord(row *recordRow) *entity.AudioRecord {
	return &entity.AudioRecord{
		Id:               row.id,
		OwnerID:          row.ownerID,
		Title:            stringOf(row.title),
		Brief:            stringOf(row.brief),
		OriginalFilename: stringOf(row.originalFilename),
		DurationMs:       numberOf(row.durationMs),
		SizeBytes:        numberOf(row.sizeBytes),
		State:            row.state,
		StateEnteredAt:   requiredTimeOf(row.stateEnteredAt),
		HaltedAt:         timeOf(row.haltedAt),
		HaltReason:       stringOf(row.haltReason),
		ErrorText:        stringOf(row.errorText),
		AcquisitionID:    stringOf(row.acquisitionID),
		AcquireExpiresAt: timeOf(row.acquireExpiresAt),
		DelayTime:        timeOf(row.delayTime),
		Attempts:         row.attempts,
		OriginalFileID:   stringOf(row.originalFileID),
		NormalizedFileID: stringOf(row.normalizedFileID),
		TranscriptTextID: stringOf(row.transcriptTextID),
		LiteraryTextID:   stringOf(row.literaryTextID),
		StructureID:      stringOf(row.structureID),
		RecognitionID:    stringOf(row.recognitionID),
		TopicIDs:         []string{},
		CreatedAt:        requiredTimeOf(row.createdAt),
		UpdatedAt:        requiredTimeOf(row.updatedAt),
	}
}
