package entity

import (
	"net/mail"
	"strings"
	"unicode"
	"unicode/utf8"
)

// MaxProviderLoginLength — предел длины логина у провайдера.
//
// Значение приходит заголовком, то есть целиком задаётся тем, кто шлёт запрос, и
// без предела в колонку уехало бы столько, сколько влезет в заголовки. Число то
// же, что у имени: длиннее имени логин не бывает, а два разных предела на
// соседних колонках одной записи разошлись бы молча.
const MaxProviderLoginLength = 255

// MaxDisplayNameLength — предел длины имени, пригодного к показу.
const MaxDisplayNameLength = 255

// AcceptProviderLogin приводит пришедшее значение к годному логину либо
// отвергает его.
//
// Отвергается пустое, состоящее из одних пробельных знаков, длиннее предела и
// несущее управляющие знаки. Пустое значение — не крайний случай: обратный
// прокси штатно шлёт заголовок пустым там, где никого не назвал, и без этой
// проверки все неназванные собрались бы в одну учётную запись с общим архивом.
//
// Обрамляющие пробелы срезаются: заголовок с ведущим пробелом и без него
// называет одного человека, а две записи о нём разошлись бы молча. Сравнение при
// поиске остаётся точным, знак в знак: приведение регистра завело бы правило,
// которого у провайдера нет.
//
// Живёт в домене, а не в транспорте и не в хранилище: правило одно на все
// способы представиться, а уложенное куском в слой оно разошлось бы двумя
// копиями при первом же втором способе.
func AcceptProviderLogin(value string) (string, bool) {
	login := strings.TrimSpace(value)

	// Предел считается в **знаках**, а не в байтах: колонка считает знаки, и два
	// предела в разных единицах разошлись бы вдвое на любой кириллице.
	if login == "" || utf8.RuneCountInString(login) > MaxProviderLoginLength {
		return "", false
	}

	for _, r := range login {
		if unicode.IsControl(r) {
			return "", false
		}
	}

	return login, true
}

// AcceptDisplayName приводит имя к годному для колонки значению.
//
// Обрезается по пределу и чистится от управляющих знаков — тем же приёмом, каким
// приём записи чистит имя файла отправителя. Пустое значение законно: имени у
// человека может не быть вовсе, и негодное значение необязательного поля не
// вправе отменять заведение учётной записи.
func AcceptDisplayName(value string) string {
	name := strings.TrimSpace(stripControls(value))

	runes := []rune(name)
	if len(runes) > MaxDisplayNameLength {
		return string(runes[:MaxDisplayNameLength])
	}

	return name
}

// AcceptEmail отдаёт адрес почты, если он вообще похож на адрес.
//
// Негодный отбрасывается здесь, а не отказом сохранения: иначе опечатка в
// заголовке кончалась бы либо отказом сервиса, либо — что хуже — ветвью «почта
// занята», и владелец искал бы общий ящик там, где сломан контур.
func AcceptEmail(value string) (string, bool) {
	email := strings.TrimSpace(value)
	if email == "" {
		return "", false
	}

	// Разбор адреса принимает и форму «Имя <адрес>»: такую строку заголовок
	// приносить не должен, и адресом она не считается.
	parsed, err := mail.ParseAddress(email)
	if err != nil || parsed.Name != "" || parsed.Address != email {
		return "", false
	}

	return email, true
}

// stripControls убирает управляющие знаки: они приезжают заголовком и в колонке
// им делать нечего.
func stripControls(value string) string {
	return strings.Map(func(r rune) rune {
		if unicode.IsControl(r) {
			return -1
		}
		return r
	}, value)
}
