package main

import (
	"encoding/base64"
	"errors"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	imap "github.com/emersion/go-imap"
	"github.com/emersion/go-imap/client"
	"github.com/emersion/go-message/mail"
	"gopkg.in/yaml.v2"
)

const (
	layout string = "2006-01-02T15:04:05Z07:00"
)

type Monitor struct {
	IMAPUri           string `yaml:"IMAPUri"`
	Username          string `yaml:"Username"`
	Password          string `yaml:"Password"`
	SourceFolder      string `yaml:"SourceFolder"`
	DestinationFolder string `yaml:"DestinationFolder"`
	TLS               bool   `yaml:"TLS"`
	DebugLevel        int64  `yaml:"DebugLevel"`
	Client            *client.Client
	FetchSet          *imap.SeqSet
	ReadSet           *imap.SeqSet
	MessageChan       chan *imap.Message
	DoneChan          chan error
}

func NewMonitor(IMAPUri, Username, Password, SourceFolder, DestinationFolder string, TLS bool) (Monitor, error) {
	m := Monitor{
		IMAPUri:           IMAPUri,
		Username:          Username,
		Password:          Password,
		TLS:               TLS,
		FetchSet:          &imap.SeqSet{},
		ReadSet:           &imap.SeqSet{},
		SourceFolder:      SourceFolder,
		DestinationFolder: DestinationFolder,
		MessageChan:       make(chan *imap.Message, 10),
		DoneChan:          make(chan error),
	}
	if TLS {
		cl, err := client.DialTLS(m.IMAPUri, nil)
		if err != nil {
			return m, err
		}
		m.Client = cl
		return m, nil
	} else {
		cl, err := client.Dial(m.IMAPUri)
		if err != nil {
			return m, err
		}
		err = cl.StartTLS(
			nil,
		)

		if err != nil {
			return m, err
		}
		m.Client = cl
		return m, nil
	}
}

func NewMonitorFromFile(path string) (Monitor, error) {
	m := Monitor{
		FetchSet:    &imap.SeqSet{},
		ReadSet:     &imap.SeqSet{},
		MessageChan: make(chan *imap.Message, 10),
		DoneChan:    make(chan error),
	}

	if _, err := os.Stat(path); err != nil {
		return m, errors.New("File does not exist\n")
	}

	f, err := os.ReadFile(path)
	if err != nil {
		return m, errors.New("File can't be opened\n")
	}

	err = yaml.Unmarshal(f, &m)
	if err != nil {
		return m, errors.New("File can't be read\n")
	}
	log.Printf("Accessing E-Mail-Account %s on %s and moving unread Messages from %s to %s", m.Username, m.IMAPUri, m.SourceFolder, m.DestinationFolder)
	return m, nil
}

func (m *Monitor) Login() error {
	if m.DebugLevel > 0 {
		log.Printf("Logging in as %s on %s. TLS: %t\n", m.Username, m.IMAPUri, m.TLS)
	}
	if m.TLS {
		if m.DebugLevel > 0 {
			log.Printf("Dialing TLS...\n")
		}
		cl, err := client.DialTLS(m.IMAPUri, nil)
		if err != nil {
			return err
		}
		m.Client = cl
	} else {
		if m.DebugLevel > 0 {
			log.Printf("Dialing without TLS...\n")
		}
		cl, err := client.Dial(m.IMAPUri)
		if err != nil {
			return err
		}
		if m.DebugLevel > 0 {
			log.Printf("Starting TLS...\n")
		}
		err = cl.StartTLS(
			nil,
		)
		if err != nil {
			return err
		}
		m.Client = cl
	}
	if m.DebugLevel > 0 {
		log.Printf("Sending Login Request...\n")
	}
	err := m.Client.Login(m.Username, m.Password)
	if err != nil {
		log.Printf("%s - Login failed: %s", m.Username, err)
	}
	if m.DebugLevel > 1 {
		m.Client.SetDebug(os.Stderr)
	}
	log.Println("Logged in!")
	return err
}

func (m *Monitor) CheckConnection() error {
	if m.DebugLevel > 0 {
		log.Printf("Checking ConnectionState: %d\n", m.Client.State())
	}
	if m.Client.State() != imap.ConnectedState &&
		m.Client.State() != imap.AuthenticatedState &&
		m.Client.State() != imap.SelectedState {
		err := m.Login()
		if err != nil {
			log.Printf("%s - Login failed: %s", m.Username, err)
			return err
		}
	}
	return nil
}

func (m *Monitor) Fetch() {
	if m.DebugLevel > 0 {
		log.Printf("Fetching unread Messages from Folder: %s\n", m.SourceFolder)
	}
	if err := m.CheckConnection(); err != nil {
		log.Printf("%s - Connection lost: %s", m.Username, err)
		return
	}

	if m.DebugLevel > 0 {
		log.Printf("Preparing Search Request by Selecting Folder: %s\n", m.SourceFolder)
	}
	criteria := imap.NewSearchCriteria()
	criteria.WithoutFlags = []string{imap.SeenFlag}
	_, err := m.Client.Select(m.SourceFolder, false)
	if err != nil {
		log.Println(err)
	}

	if m.DebugLevel > 0 {
		log.Printf("Sending Search Request...\n")
	}
	ids, err := m.Client.Search(criteria)
	if err != nil {
		log.Println(err)
	}

	if m.DebugLevel > 0 {
		log.Printf("Found %d Messages in %s\n", len(ids), m.SourceFolder)
	}
	if len(ids) <= 0 {
		return
	}

	m.FetchSet.AddNum(ids...)

	if m.DebugLevel > 0 {
		log.Printf("Fetching %d Messages from %s\n", len(ids), m.SourceFolder)
	}
	go func() {
		m.DoneChan <- m.Client.Fetch(m.FetchSet, []imap.FetchItem{"BODY.PEEK[]"}, m.MessageChan) //imap.FetchRFC822}, m.MessageChan)
	}()

	err = m.PrintAll()
	if err != nil {
		log.Println(err)
	}
	log.Printf("Fetching done\n")
}

func (m *Monitor) MarkRead() {
	if m.DebugLevel > 0 {
		log.Printf("Marking Messages read in Folder: %s\n", m.DestinationFolder)
	}

	if err := m.CheckConnection(); err != nil {
		log.Printf("%s - Connection lost: %s", m.Username, err)
		return
	}

	if m.DebugLevel > 0 {
		log.Printf("Preparing Search Request by Selecting Folder: %s\n", m.DestinationFolder)
	}
	criteria := imap.NewSearchCriteria()
	criteria.WithoutFlags = []string{imap.SeenFlag}
	_, err := m.Client.Select(m.DestinationFolder, false)
	if err != nil {
		log.Println("Marking read failed: ", err)
	}

	if m.DebugLevel > 0 {
		log.Printf("Sending Search Request...\n")
	}
	ids, err := m.Client.Search(criteria)
	if err != nil {
		log.Println("Getting unread messages: ", err)
	}

	if m.DebugLevel > 0 {
		log.Printf("Found %d Messages in %s\n", len(ids), m.DestinationFolder)
	}
	if len(ids) <= 0 {
		log.Println("No unread Messages found.")
		return
	}

	m.ReadSet.Clear()
	m.ReadSet.AddNum(ids...)

	if m.DebugLevel > 0 {
		log.Printf("Marking %d Messages read in %s\n", len(ids), m.DestinationFolder)
	}
	item := imap.FormatFlagsOp(imap.AddFlags, true)
	flags := []interface{}{imap.SeenFlag}
	err = m.Client.Store(m.ReadSet, item, flags, nil)
	if err != nil {
		log.Fatal("Marking as read: ", err)
	}
	if m.DebugLevel > 0 {
		log.Printf("Marked %d Messages read in %s\n", len(ids), m.DestinationFolder)
	}
	log.Printf("Marking read done\n")
}

func (m *Monitor) PrintAll() error {
	moveSeqSet := new(imap.SeqSet)
	if m.DebugLevel > 0 {
		log.Printf("Downloading & sending fetched Messages to Default Printer\n")
	}

	for msg := range m.MessageChan {
		var file string
		var err error
		var subject string
		var from string
		var to string
		var cc string
		var date string
		var header []string
		var html int
		var text int

		timestamp := time.Now().Format(layout)

		file = filepath.Join(os.TempDir(), (base64.StdEncoding.EncodeToString([]byte(timestamp))))

		filehtml := file + ".html"
		filetxt := file + ".txt"

		fhtml, err := os.Create(filehtml)
		if err != nil {
			log.Println("File Creation: ", err)
			return err
		}
		defer fhtml.Close()

		ftxt, err := os.Create(filetxt)
		if err != nil {
			log.Println("File Creation: ", err)
			return err
		}
		defer ftxt.Close()

		for _, literal := range msg.Body {
			mr, err := mail.CreateReader(literal)
			if err != nil {
				log.Fatal("Creating Reader:", err)
				return err
			}

			fr, _ := mr.Header.AddressList("From")
			for _, a := range fr {
				from += a.Address + ","
			}
			from = from[:len(from)-1]

			t, _ := mr.Header.AddressList("To")
			for _, a := range t {
				to += a.Address + ","
			}
			to = to[:len(to)-1]

			c, _ := mr.Header.AddressList("Cc")
			if len(c) > 0 {
				for _, a := range c {
					cc += a.Address + ","
				}
				cc = cc[:len(cc)-1]
			}
			date = mr.Header.Get("Date")
			subject = mr.Header.Get("Subject")

			if m.DebugLevel > 0 {
				log.Println(date, from, to, subject, cc)
			}

			if "" != date {
				header = append(
					header,
					("Date:\t\t" + date + "\n"),
				)
			}
			if "" != from {
				header = append(
					header,
					("From:\t\t" + from + "\n"),
				)
			}
			if "" != to {
				header = append(
					header,
					("To:\t\t" + to + "\n"),
				)
			}
			if "" != cc {
				header = append(
					header,
					("Cc:\t\t" + cc + "\n"),
				)
			}
			if "" != subject {
				header = append(
					header,
					("Subject:\t" + subject + "\n"),
				)
			}

			header = append(
				header,
				("====================================\n\n\n\n"),
			)

			for _, s := range header {
				fhtml.WriteString(s)
				if err != nil {
					log.Printf("Writing File (Header): %s\n", err)
				}
				ftxt.WriteString(s)
				if err != nil {
					log.Printf("Writing File (Header): %s\n", err)
				}
			}
			for {
				p, err := mr.NextPart()
				if err == io.EOF {
					break
				} else if err != nil {
					log.Printf("Reading Message: %s\n", err)
					return err
				}

				switch h := p.Header.(type) {
				case *mail.InlineHeader:

					t, _, err := h.ContentType()
					if err != nil {
						log.Printf("Reading ContentType: %s\n", err)
						return err
					}
					switch t {
					case "text/html":
						htmltmp := parseMessage(p.Body, fhtml)
						if htmltmp > 0 {
							html = htmltmp
						}
					case "text/plain":
						texttmp := parseMessage(p.Body, ftxt)
						if texttmp > 0 {
							text = texttmp
						}
					default:
						continue
					}
				}
			}
			moveSeqSet.AddNum(msg.Uid)
			err = m.Client.Move(moveSeqSet, m.DestinationFolder)
			if err != nil {
				log.Println("Moving Item: ", err)
			}
		}
		if text > 0 {
			file = filetxt
		} else if html > 0 {
			file = filehtml
		}

		err = printLinux(file, true)
		if err != nil {
			log.Println("Printing: ", err)
			return err
		}
	}

	m.MessageChan = make(chan *imap.Message)
	return nil
}

func parseMessage(r io.Reader, f *os.File) int {
	var err error
	content, _ := io.ReadAll(r)
	i, err := f.Write(content)
	if err != nil {
		log.Printf("Writing File (%s): %s", f.Name(), err)
	}
	return i
}

func CheckPrerequisites() error {
	var err error
	_, err = exec.LookPath("wkhtmltopdf")
	if err != nil {
		log.Println(err)
		return err
	}
	_, err = exec.LookPath("lp")
	if err != nil {
		log.Println(err)
		return err
	}
	return err
}

func printLinux(path string, delete bool) error {
	lp, err := exec.LookPath("lp")
	if err != nil {
		log.Println(err)
		return err
	}
	if strings.Contains(path, "html") {
		npath := strings.ReplaceAll(path, ".html", ".pdf")

		wkhtmltopdf, err := exec.LookPath("wkhtmltopdf")
		if err != nil {
			log.Println(err)
			return err
		}

		cmd := exec.Command(wkhtmltopdf, path, npath)
		_ = cmd.Run()
		if delete {
			err = os.Remove(path)
			if err != nil {
				log.Println(err)
			}
		}
		path = npath
	}
	cmd := exec.Command(lp, path)
	_ = cmd.Run()

	if delete {
		err = os.Remove(path)
		if err != nil {
			log.Println(err)
		}
	}

	return err
}

func Run() {
	var m Monitor
	var err error

	argLength := len(os.Args[1:])
	if err := CheckPrerequisites(); err != nil {
		log.Fatal(err)
	}
	switch {
	case argLength <= 0:
		if _, err := os.Stat("settings.yaml"); err != nil {
			log.Fatal("Required Arguments missing!")
		}
		m, err = NewMonitorFromFile("settings.yaml")
		if err != nil {
			log.Fatal(err)
		}

	case argLength >= 1:
		m, err = NewMonitorFromFile(os.Args[1])
		if err != nil {
			log.Fatal(err)
		}

		if argLength > 1 {
			d, err := strconv.ParseInt(os.Args[2], 10, 64)
			if err != nil {
				log.Fatal(err)
			}
			m.DebugLevel = d
		}

	default:
		log.Fatal("Invalid or missing Parameters. Make sure to pass the pass to the 'settings.yaml'")
	}
	err = m.Login()
	if err != nil {
		log.Fatal(err)
	}
	m.Fetch()
	m.MarkRead()
}