package router

// PythonDateParser — the production DateTimeParser. Shells out to python3
// with the `dateparser` library (BSD-3, 200+ locales, ru+en native) for full
// natural-language date/time extraction: "через два часа", "в следующую
// пятницу", "завтра в 9 утра", "in 30 minutes", "on friday at noon".
//
// Falls back to StubDateTimeParser when python3 or dateparser is unavailable
// (e.g. dev runs without the Docker runtime image). The stub handles the
// common patterns; this handles the long tail.
//
// The interface is the seam — wireVoice swaps implementations without
// touching the extractor or the daemon. This struct is safe for concurrent
// use: each Parse call spawns an independent python process.

import (
	"context"
	"log"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

// dateparserScript — the python inline script. Receives text as argv[1] and
// the current time as an ISO 8601 string (argv[2]). Prints the parsed
// datetime as a Unix timestamp (seconds.micros) on stdout, or nothing if no
// date was found. Exits non-zero on import failure (dateparser not
// installed).
//
// Two-step parse: search_dates finds the date substring in surrounding text
// ("напомни через час" → "через час"), then parse() re-parses that substring
// for correct time resolution (search_dates mishandles AM/PM). Russian time
// qualifiers (утра/вечера/дня/ночи) are pre-processed to AM/PM because
// dateparser drops them during substring extraction.
const dateparserScript = `import sys, re
from datetime import datetime
try:
    import dateparser
    from dateparser.search import search_dates
except ImportError:
    sys.exit(1)
try:
    text = sys.argv[1]
    now = datetime.fromisoformat(sys.argv[2])
    # Pre-process: replace Russian time qualifiers with AM/PM.
    # Handles "9 утра", "10 часов утра", "3 часа дня" etc.
    text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?утра\b', r'\1 am', text, flags=re.IGNORECASE)
    text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?вечера\b', r'\1 pm', text, flags=re.IGNORECASE)
    text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?дня\b', r'\1 pm', text, flags=re.IGNORECASE)
    text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?ночи\b', r'\1 am', text, flags=re.IGNORECASE)
    # A bare hour after a preposition is dropped on the floor by dateparser:
    # "завтра в 7" resolves to tomorrow at the CURRENT clock, and "завтра в 7
    # часов" is read as seven hours from now. Only a qualifier (already an
    # am/pm above) or a colon makes it read the hour, so give it the colon.
    # English "at 7" fails identically, so both prepositions are rewritten.
    # "на 9" is the same hour said with the other preposition, and it was not
    # read at all until V-579: "в 9" set the reminder and "на 9" did not.
    # "к двум часам" is a third preposition and the dative that goes with it,
    # and it was read as no time at all until V-609.
    # The preposition is normalised as well as the hour (V-610). dateparser
    # joins a day word to a clock through "в" and through no other Russian
    # preposition, so "завтра к 03:00 pm" loses the clock and resolves to
    # tomorrow at the CURRENT minute. "на" was silently losing it the same way.
    def _at(m):
        prep = 'at' if m.group(1).lower() in ('at', 'by') else 'в'
        return '%s %02d:00' % (prep, int(m.group(2)))
    text = re.sub(r'(?<![\w:])(в|во|на|к|ко|at|by)\s+([01]?\d|2[0-3])(?:\s+час(?:а|ов|у|ам)?)?(?![\d:.\w])',
                  _at, text, flags=re.IGNORECASE)
    settings = {'PREFER_DATES_FROM': 'future', 'RELATIVE_BASE': now}
    # Two-step: search_dates finds the date substring in text,
    # parse() gets the time right (search_dates mishandles AM/PM).
    results = search_dates(text, languages=['ru', 'en'], settings=settings)
    if results:
        substr, raw_dt = results[0]
        dt = dateparser.parse(substr, languages=['ru', 'en'], settings=settings)
        if dt is None:
            dt = raw_dt
        if dt is not None:
            print(dt.timestamp())
except Exception:
    pass
`

// PythonDateParser implements DateTimeParser via the python dateparser
// library, with a StubDateTimeParser fallback for environments where
// python3/dateparser isn't installed.
type PythonDateParser struct {
	fallback DateTimeParser
}

// NewPythonDateParser constructs a PythonDateParser with StubDateTimeParser
// as the fallback.
func NewPythonDateParser() *PythonDateParser {
	return &PythonDateParser{fallback: StubDateTimeParser{}}
}

// Parse extracts a datetime from text using python's dateparser. If python3
// or dateparser is unavailable, falls back to the stub parser. Returns
// (time, true, nil) on success; (zero, false, nil) when no date is found.
func (p *PythonDateParser) Parse(ctx context.Context, text string, now time.Time) (time.Time, bool, error) {
	// Speech says the hour in words, and neither this parser nor the stub
	// reads "в семь вечера" (Vikunja #469). Both see the digits instead.
	text = SpellOutDigits(text)
	t, ok, err := p.parseWithPython(ctx, text, now)
	if err != nil {
		// python3 missing, dateparser not installed, or process failure —
		// degrade to the stub which handles the common cases.
		log.Printf("router: python dateparser unavailable, falling back to stub: %v", err)
		return p.fallback.Parse(ctx, text, now)
	}
	if ok {
		t = rollPastClockForward(t, now, text)
	}
	return t, ok, nil
}

// rollPastClockForward moves a clock that has already gone by to its next
// occurrence.
//
// dateparser is handed PREFER_DATES_FROM future and does not apply it to an
// HH:MM time on today's date, so at 14:41 "напомни в половине первого пообедать"
// resolved to 12:30 the same day and the reminder was two hours in the past
// (V-544). parseClock in the stub has always rolled forward, so this is the
// production parser agreeing with the floor rather than a new rule.
//
// Only a bare clock rolls. A sentence that names its day keeps it, so a
// deliberate "сегодня в 12:30" stays where he put it, and the backdated write
// path (V-518) is a different seam entirely. Past by a day or more is not a
// clock resolved onto today, so it is left alone too.
func rollPastClockForward(t, now time.Time, text string) time.Time {
	if t.After(now) || now.Sub(t) >= 24*time.Hour || NamesADay(text) {
		return t
	}
	return t.Add(24 * time.Hour)
}

// parseWithPython runs the dateparser script and parses the timestamp output.
// Returns (zero, false, error) on process/exec failure; (zero, false, nil)
// when the script ran but found no date.
func (p *PythonDateParser) parseWithPython(ctx context.Context, text string, now time.Time) (time.Time, bool, error) {
	cmd := exec.CommandContext(ctx, "python3", "-c", dateparserScript, text, now.Format(time.RFC3339))
	output, err := cmd.Output()
	if err != nil {
		return time.Time{}, false, err
	}
	s := strings.TrimSpace(string(output))
	if s == "" {
		return time.Time{}, false, nil
	}
	f, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return time.Time{}, false, nil
	}
	sec := int64(f)
	nsec := int64((f - float64(sec)) * 1e9)
	return time.Unix(sec, nsec).Local(), true, nil
}
