<commit_before>package notifier

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/oinume/lekcije/server/config"
	"github.com/oinume/lekcije/server/errors"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
)

var lessonFetcher *fetcher.TeacherLessonFetcher

func init() {
	http.DefaultClient.Timeout = 5 * time.Second
	lessonFetcher = fetcher.NewTeacherLessonFetcher(http.DefaultClient, nil)
}

type Notifier struct {
	dryRun   bool
	teachers map[uint32]*model.Teacher
}

func NewNotifier(dryRun bool) *Notifier {
	return &Notifier{
		dryRun:   dryRun,
		teachers: make(map[uint32]*model.Teacher, 1000),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	teacherIds, err := model.FollowingTeacherService.FindTeacherIdsByUserId(user.Id)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIdsByUserId(): userId=%v", user.Id)
	}

	availableLessonsPerTeacher := make(map[uint32][]*model.Lesson, 1000)
	allFetchedLessons := make([]*model.Lesson, 0, 5000)
	for _, teacherId := range teacherIds {
		teacher, fetchedLessons, availableLessons, err := n.fetchAndExtractAvailableLessons(teacherId)
		if err != nil {
			return err
		}

		allFetchedLessons = append(allFetchedLessons, fetchedLessons...)
		n.teachers[teacherId] = teacher
		for _, l := range availableLessons {
			fmt.Printf("available -> teacherId:%v, datetime:%v, status:%v \n", l.TeacherId, l.Datetime.Format("2006-01-02 15:04"), l.Status)
		}
		if len(availableLessons) > 0 {
			availableLessonsPerTeacher[teacherId] = availableLessons
		}
	}

	if err := n.sendNotificationToUser(user, availableLessonsPerTeacher); err != nil {
		return err
	}

	if !n.dryRun {
		model.LessonService.UpdateLessons(allFetchedLessons)
	}

	return nil
}

// Returns teacher, fetchedLessons, availableLessons, error
func (n *Notifier) fetchAndExtractAvailableLessons(teacherId uint32) (
	*model.Teacher, []*model.Lesson, []*model.Lesson, error,
) {
	teacher, fetchedLessons, err := lessonFetcher.Fetch(teacherId)
	if err != nil {
		return nil, nil, nil, err
	}
	//fmt.Printf("--- %s ---\n", teacher.Name)
	//for _, lesson := range fetchedLessons {
	//	fmt.Printf("datetime = %v, status = %v\n", lesson.Datetime, lesson.Status)
	//}

	now := time.Now()
	fromDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, config.LocalTimezone())
	toDate := fromDate.Add(24 * 6 * time.Hour)
	lastFetchedLessons, err := model.LessonService.FindLessons(teacher.Id, fromDate, toDate)
	if err != nil {
		return nil, nil, nil, err
	}
	availableLessons := model.LessonService.GetNewAvailableLessons(lastFetchedLessons, fetchedLessons)
	return teacher, fetchedLessons, availableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User,
	lessonsPerTeacher map[uint32][]*model.Lesson,
) error {
	lessonsCount := 0
	var teacherIds []int
	for teacherId, lessons := range lessonsPerTeacher {
		teacherIds = append(teacherIds, int(teacherId))
		lessonsCount += len(lessons)
	}
	if lessonsCount == 0 {
		// Don't send notification
		return nil
	}

	sort.Ints(teacherIds)
	var teacherIds2 []uint32
	var teacherNames []string
	for _, id := range teacherIds {
		teacherIds2 = append(teacherIds2, uint32(id))
		teacherNames = append(teacherNames, n.teachers[uint32(id)].Name)
	}

	t := template.New("email")
	t = template.Must(t.Parse(getEmailTemplate()))
	type TemplateData struct {
		TeacherIds        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32][]*model.Lesson
	}
	data := &TemplateData{
		TeacherIds:        teacherIds2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher,
	}

	var body bytes.Buffer
	if err := t.Execute(&body, data); err != nil {
		return errors.InternalWrapf(err, "Failed to execute template.")
	}
	//fmt.Printf("--- mail ---\n%s", body.String())

	subject := "[lekcije] Schedules of teacher " + strings.Join(teacherNames, ", ")
	sender := &EmailNotificationSender{}
	return sender.Send(user, subject, body.String())
}

func getEmailTemplate() string {
	return strings.TrimSpace(`
{{- range $teacherId := .TeacherIds }}
{{- $teacher := index $.Teachers $teacherId -}}
--- Teacher '{{ $teacher.Name }}' available lessons ---
  {{- $lessons := index $.LessonsPerTeacher $teacherId -}}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime }}
  {{- end }}

{{ end }}
	`)
}

type NotificationSender interface {
	Send(user *model.User, subject, body string) error
}

type EmailNotificationSender struct{}

func (s *EmailNotificationSender) Send(user *model.User, subject, body string) error {
	from := mail.NewEmail("noreply", "<EMAIL>") // TODO: <EMAIL>
	to := mail.NewEmail(user.Name, user.Email.Raw())
	content := mail.NewContent("text/html", strings.Replace(body, "\n", "<br>", -1))
	m := mail.NewV3MailInit(from, subject, to, content)

	req := sendgrid.GetRequest(
		os.Getenv("SENDGRID_API_KEY"),
		"/v3/mail/send",
		"https://api.sendgrid.com",
	)
	req.Method = "POST"
	req.Body = mail.GetRequestBody(m)
	resp, err := sendgrid.API(req)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to send email by sendgrid")<commit_msg>Organize email format<commit_after>package notifier

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/oinume/lekcije/server/config"
	"github.com/oinume/lekcije/server/errors"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
)

var lessonFetcher *fetcher.TeacherLessonFetcher

func init() {
	http.DefaultClient.Timeout = 5 * time.Second
	lessonFetcher = fetcher.NewTeacherLessonFetcher(http.DefaultClient, nil)
}

type Notifier struct {
	dryRun   bool
	teachers map[uint32]*model.Teacher
}

func NewNotifier(dryRun bool) *Notifier {
	return &Notifier{
		dryRun:   dryRun,
		teachers: make(map[uint32]*model.Teacher, 1000),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	teacherIds, err := model.FollowingTeacherService.FindTeacherIdsByUserId(user.Id)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIdsByUserId(): userId=%v", user.Id)
	}

	availableLessonsPerTeacher := make(map[uint32][]*model.Lesson, 1000)
	allFetchedLessons := make([]*model.Lesson, 0, 5000)
	for _, teacherId := range teacherIds {
		teacher, fetchedLessons, availableLessons, err := n.fetchAndExtractAvailableLessons(teacherId)
		if err != nil {
			return err
		}

		allFetchedLessons = append(allFetchedLessons, fetchedLessons...)
		n.teachers[teacherId] = teacher
		if len(availableLessons) > 0 {
			availableLessonsPerTeacher[teacherId] = availableLessons
		}
	}

	if err := n.sendNotificationToUser(user, availableLessonsPerTeacher); err != nil {
		return err
	}

	if !n.dryRun {
		model.LessonService.UpdateLessons(allFetchedLessons)
	}

	return nil
}

// Returns teacher, fetchedLessons, availableLessons, error
func (n *Notifier) fetchAndExtractAvailableLessons(teacherId uint32) (
	*model.Teacher, []*model.Lesson, []*model.Lesson, error,
) {
	teacher, fetchedLessons, err := lessonFetcher.Fetch(teacherId)
	if err != nil {
		return nil, nil, nil, err
	}
	//fmt.Printf("--- %s ---\n", teacher.Name)
	//for _, lesson := range fetchedLessons {
	//	fmt.Printf("datetime = %v, status = %v\n", lesson.Datetime, lesson.Status)
	//}

	now := time.Now()
	fromDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, config.LocalTimezone())
	toDate := fromDate.Add(24 * 6 * time.Hour)
	lastFetchedLessons, err := model.LessonService.FindLessons(teacher.Id, fromDate, toDate)
	if err != nil {
		return nil, nil, nil, err
	}
	availableLessons := model.LessonService.GetNewAvailableLessons(lastFetchedLessons, fetchedLessons)
	return teacher, fetchedLessons, availableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User,
	lessonsPerTeacher map[uint32][]*model.Lesson,
) error {
	lessonsCount := 0
	var teacherIds []int
	for teacherId, lessons := range lessonsPerTeacher {
		teacherIds = append(teacherIds, int(teacherId))
		lessonsCount += len(lessons)
	}
	if lessonsCount == 0 {
		// Don't send notification
		return nil
	}

	sort.Ints(teacherIds)
	var teacherIds2 []uint32
	var teacherNames []string
	for _, id := range teacherIds {
		teacherIds2 = append(teacherIds2, uint32(id))
		teacherNames = append(teacherNames, n.teachers[uint32(id)].Name)
	}

	t := template.New("email")
	t = template.Must(t.Parse(getEmailTemplate()))
	type TemplateData struct {
		TeacherIds        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32][]*model.Lesson
	}
	data := &TemplateData{
		TeacherIds:        teacherIds2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher,
	}

	var body bytes.Buffer
	if err := t.Execute(&body, data); err != nil {
		return errors.InternalWrapf(err, "Failed to execute template.")
	}
	//fmt.Printf("--- mail ---\n%s", body.String())

	subject := "[lekcije] Schedule of teacher " + strings.Join(teacherNames, ", ")
	sender := &EmailNotificationSender{}
	return sender.Send(user, subject, body.String())
}

func getEmailTemplate() string {
	return strings.TrimSpace(`
{{- range $teacherId := .TeacherIds }}
{{- $teacher := index $.Teachers "$teacherId" -}}
--- Available lessons of Teacher {{ $teacher.Name }} ---
PC: http://eikaiwa.dmm.com/teacher/index/{{ $teacherId }}/
Mobile: http://eikaiwa.dmm.com/teacher/schedule/{{ $teacherId }}/

  {{- $lessons := index $.LessonsPerTeacher $teacherId -}}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

{{ end }}
	`)
}

type NotificationSender interface {
	Send(user *model.User, subject, body string) error
}

type EmailNotificationSender struct{}

func (s *EmailNotificationSender) Send(user *model.User, subject, body string) error {
	from := mail.NewEmail("noreply", "<EMAIL>") // TODO: <EMAIL>
	to := mail.NewEmail(user.Name, user.Email.Raw())
	content := mail.NewContent("text/html", strings.Replace(body, "\n", "<br>", -1))
	m := mail.NewV3MailInit(from, subject, to, content)

	req := sendgrid.GetRequest(
		os.Getenv("SENDGRID_API_KEY"),
		"/v3/mail/send",
		"https://api.sendgrid.com",
	)
	req.Method = "POST"
	req.Body = mail.GetRequestBody(m)
	resp, err := sendgrid.API(req)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to send email by sendgrid")
{'fraction_non_alphanumeric': 0.1499670091431803, 'fraction_numerical': 0.012253746818738807, 'mean_word_length': 4.560796645702306, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 12, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3715575', 'n_tokens_mistral': 4039, 'n_tokens_neox': 3800, 'n_words': 775}