// Package nsdxml реализует кодек XML НРД для M2M: парсер/сериализатор
// windows-1251 и тип NSDDateTime для нестандартного формата отметки
// времени, используемого в XSD НРД ("YYYY-MM-DDThh:mm:ss(МСК+N)").
package nsdxml

import (
	"encoding/xml"
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"time"
)

// moscowLocation — фиксированная локация UTC+3 для отметок МСК.
// Используется именованная локация (без обращения к tzdata) — на стенде
// может не быть установленной базы IANA.
var moscowLocation = time.FixedZone("МСК", 3*60*60)

// nsdDateTimeRegex — паттерн из XSD M2MTypesNSD.xsd
// (NSDDateTimeType): дата T время затем (МСК) либо (МСК+N) / (МСК-N).
var nsdDateTimeRegex = regexp.MustCompile(
	`^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T` +
		`([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])` +
		`\(МСК(([+-])([0-9]{1,2}))?\)$`,
)

// NSDDateTime — отметка времени в формате НРД. Внутреннее
// представление — время в зоне Europe/Moscow (UTC+3) плюс смещение в
// часах относительно МСК, если оно указано в исходной строке.
type NSDDateTime struct {
	// Time хранится в локации "МСК" (UTC+3). При наличии смещения сам
	// инстант сдвинут на OffsetHours часов вперёд/назад.
	Time time.Time
	// OffsetHours — сдвиг в часах относительно МСК (0 если не задан).
	OffsetHours int
	// OffsetSpecified отличает "(МСК)" (false) от "(МСК+0)" (true).
	// Сохраняется ради round-trip.
	OffsetSpecified bool
}

// Now возвращает текущее время в зоне МСК без указания сдвига.
func Now() NSDDateTime {
	return NSDDateTime{Time: time.Now().In(moscowLocation)}
}

// String возвращает строковое представление в формате НРД
// "YYYY-MM-DDThh:mm:ss(МСК[+-N])".
func (d NSDDateTime) String() string {
	mt := d.Time.In(moscowLocation)
	base := fmt.Sprintf(
		"%04d-%02d-%02dT%02d:%02d:%02d",
		mt.Year(), int(mt.Month()), mt.Day(),
		mt.Hour(), mt.Minute(), mt.Second(),
	)
	switch {
	case !d.OffsetSpecified:
		return base + "(МСК)"
	case d.OffsetHours >= 0:
		return fmt.Sprintf("%s(МСК+%d)", base, d.OffsetHours)
	default:
		return fmt.Sprintf("%s(МСК%d)", base, d.OffsetHours)
	}
}

// parseNSDDateTime разбирает строку формата НРД в NSDDateTime.
func parseNSDDateTime(s string) (NSDDateTime, error) {
	m := nsdDateTimeRegex.FindStringSubmatch(s)
	if m == nil {
		return NSDDateTime{}, fmt.Errorf("nsdxml: NSDDateTime %q не соответствует формату", s)
	}
	year, _ := strconv.Atoi(m[1])
	month, _ := strconv.Atoi(m[2])
	day, _ := strconv.Atoi(m[3])
	hour, _ := strconv.Atoi(m[4])
	min, _ := strconv.Atoi(m[5])
	sec, _ := strconv.Atoi(m[6])

	t := time.Date(year, time.Month(month), day, hour, min, sec, 0, moscowLocation)

	out := NSDDateTime{Time: t}
	if m[7] != "" {
		val, _ := strconv.Atoi(m[9])
		if m[8] == "-" {
			val = -val
		}
		out.OffsetHours = val
		out.OffsetSpecified = true
	}
	return out, nil
}

// MarshalText сериализует NSDDateTime в строку формата НРД.
func (d NSDDateTime) MarshalText() ([]byte, error) {
	return []byte(d.String()), nil
}

// UnmarshalText разбирает строку формата НРД в NSDDateTime.
func (d *NSDDateTime) UnmarshalText(b []byte) error {
	if len(b) == 0 {
		return errors.New("nsdxml: пустое значение NSDDateTime")
	}
	parsed, err := parseNSDDateTime(string(b))
	if err != nil {
		return err
	}
	*d = parsed
	return nil
}

// MarshalXML кодирует NSDDateTime в строку формата НРД.
func (d NSDDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	return e.EncodeElement(d.String(), start)
}

// UnmarshalXML декодирует строку формата НРД в NSDDateTime.
func (d *NSDDateTime) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
	var s string
	if err := dec.DecodeElement(&s, &start); err != nil {
		return err
	}
	return d.UnmarshalText([]byte(s))
}
