package matrix

import (
	"context"
	"encoding/json"
	"fmt"
	"image"
	"io"
	"log"
	"os"
	"time"

	"git.sr.ht/~guacamolie/faxmachine/config"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rs/zerolog"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/crypto/cryptohelper"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

var storeEventType = event.NewEventType("nl.guacamolie.faxmachine.store")

type Matrix struct {
	Client          *mautrix.Client
	inviteHandlers  []InviteHandler
	messageHandlers []MessageHandler
}

func NewMatrix(ctx context.Context, conf config.Matrix) (*Matrix, error) {
	me := id.UserID(conf.UserID)
	client, err := mautrix.NewClient(me.Homeserver(), me, conf.AccessToken)
	if err != nil {
		return nil, fmt.Errorf("failed to initalize client")
	}

	if conf.MautrixLog != "" {
		out, err := os.OpenFile(conf.MautrixLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
		if err != nil {
			return nil, fmt.Errorf("failed to mautrix log file %q: %w", conf.MautrixLog, err)
		}

		log := zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
			w.Out = out
			w.TimeFormat = time.RFC3339
		})).With().Timestamp().Logger()

		client.Log = log
	}

	client.DeviceID = id.DeviceID(conf.DeviceID)

	m := &Matrix{
		Client: client,
	}

	syncer := mautrix.NewDefaultSyncer()

	// Prevent infinite loop of store events
	syncer.FilterJSON = &mautrix.Filter{
		AccountData: mautrix.FilterPart{
			Limit:    20,
			NotTypes: []event.Type{storeEventType},
		},
	}

	syncer.OnEventType(event.StateMember, m.handleStateMember)
	syncer.OnEventType(event.EventMessage, m.handleMessage)

	syncer.OnEvent(func(ctx context.Context, evt *event.Event) {
		if evt.Mautrix.EventSource == event.SourcePresence {
			// Not interesting enough to log
			return
		}

		pretty, err := json.MarshalIndent(evt, "", "\t")
		if err != nil {
			log.Printf("failed to marshal: %v\n", err)
		}
		log.Printf("OnEvent %s %s\n", evt.Mautrix.EventSource, pretty)
	})

	client.Syncer = syncer

	store := mautrix.NewAccountDataStore(storeEventType.Type, client)
	client.Store = store

	if len(conf.FeelgoodKey) > 0 && conf.Database != "" {
		cryptoHelper, err := cryptohelper.NewCryptoHelper(m.Client, []byte(conf.FeelgoodKey), conf.Database)
		if err != nil {
			return nil, fmt.Errorf("failed to create crypto helper: %w", err)
		}

		if err := cryptoHelper.Init(ctx); err != nil {
			return nil, fmt.Errorf("failed to initalize crypto helper: %w", err)
		}

		m.Client.Crypto = cryptoHelper
	}

	return m, nil
}

func (m *Matrix) JoinedRooms(ctx context.Context) ([]Room, error) {
	resp, err := m.Client.JoinedRooms(ctx)
	if err != nil {
		return nil, err
	}

	rooms := []Room{}
	for _, roomID := range resp.JoinedRooms {
		rooms = append(rooms, m.Room(roomID))
	}

	return rooms, nil
}

func (m *Matrix) SetPresence(ctx context.Context, status event.Presence) {
	m.Client.SyncPresence = event.PresenceOnline
	m.Client.SetPresence(ctx, event.PresenceOnline)
}

func (m *Matrix) Room(id id.RoomID) Room {
	return Room{
		parent: m,
		ID:     id,
	}
}

func (m *Matrix) ResolveAlias(ctx context.Context, alias id.RoomAlias) (Room, error) {
	resp, err := m.Client.ResolveAlias(ctx, alias)
	if err != nil {
		return Room{}, err
	}
	return m.Room(resp.RoomID), nil
}

type Invite struct {
	parent *Matrix
	Sender id.UserID
	RoomID id.RoomID
}

func (i *Invite) Accept(ctx context.Context) error {
	_, err := i.parent.Client.JoinRoomByID(ctx, i.RoomID)
	return err
}

func (i *Invite) Decline(ctx context.Context) error {
	_, err := i.parent.Client.LeaveRoom(ctx, i.RoomID)
	return err
}

type InviteHandler func(ctx context.Context, i Invite)

func (m *Matrix) OnInvite(handler InviteHandler) {
	m.inviteHandlers = append(m.inviteHandlers, handler)
}

func (m *Matrix) handleStateMember(ctx context.Context, evt *event.Event) {
	if evt.Mautrix.EventSource&(event.SourceInvite|event.SourceState) == 0 {
		return
	}
	if evt.GetStateKey() != m.Client.UserID.String() {
		return
	}
	content := evt.Content.AsMember()
	if content.Membership != event.MembershipInvite {
		return
	}

	invite := Invite{
		parent: m,
		Sender: evt.Sender,
		RoomID: evt.RoomID,
	}

	for _, handler := range m.inviteHandlers {
		handler(ctx, invite)
	}
}

type MessageHandler func(ctx context.Context, m Message)

func (m *Matrix) OnMessage(handler MessageHandler) {
	m.messageHandlers = append(m.messageHandlers, handler)
}

func (m *Matrix) handleMessage(ctx context.Context, evt *event.Event) {
	if evt.Sender == m.Client.UserID {
		return
	}

	baseMessage := message{
		parent: m,
		evt:    evt,
	}

	var message Message = &baseMessage

	switch evt.Content.AsMessage().MsgType {
	case event.MsgText:
		message = &TextMessage{baseMessage}
	case event.MsgImage:
		message = &ImageMessage{baseMessage}
	}

	for _, handler := range m.messageHandlers {
		handler(ctx, message)
	}
}

type Message interface {
	ID() id.EventID
	Time() time.Time
	Sender() id.UserID
	Room() Room
	SendReceipt(ctx context.Context) error
	Reply(ctx context.Context, reply string) error
	React(ctx context.Context, reaction string) error
}

type message struct {
	parent *Matrix
	evt    *event.Event
}

func (m *message) ID() id.EventID {
	return m.evt.ID
}

func (m *message) Time() time.Time {
	return time.UnixMilli(m.evt.Timestamp)
}

func (m *message) Sender() id.UserID {
	return m.evt.Sender
}

func (m *message) Room() Room {
	return m.parent.Room(m.evt.RoomID)
}

func (m *message) SendReceipt(ctx context.Context) error {
	return m.parent.Client.
		SendReceipt(ctx, m.evt.RoomID, m.evt.ID, event.ReceiptTypeRead, nil)
}

func (m *message) React(ctx context.Context, reaction string) error {
	_, err := m.parent.Client.SendReaction(ctx, m.evt.RoomID, m.evt.ID, reaction)
	return err
}

func (m *message) Reply(ctx context.Context, reply string) error {
	content := &event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    reply,
	}

	// mautrix-go currently does not generate the correct fallback for m.file
	// and related events:
	//
	//   https://spec.matrix.org/v1.8/client-server-api/#fallback-for-mimage-mvideo-maudio-and-mfile
	//
	// TODO: consider sending a patch to support this.
	content.SetReply(m.evt)

	_, err := m.parent.Client.SendMessageEvent(ctx, m.evt.RoomID, event.EventMessage, content)
	return err
}

type TextMessage struct {
	message
}

func (tm *TextMessage) Body() string {
	msg := tm.evt.Content.AsMessage()
	if msg.MsgType != event.MsgText {
		panic(fmt.Sprintf("expected msgtype of m.text, got %s", msg.MsgType))
	}

	return msg.Body
}

type ImageMessage struct {
	message
}

func (im *ImageMessage) DownloadImage(ctx context.Context) (image.Image, error) {
	msg := im.evt.Content.AsMessage()
	if msg.MsgType != event.MsgImage {
		panic(fmt.Sprintf("expected msgtype of m.image, got %s", msg.MsgType))
	}

	var imageReader io.ReadCloser

	if msg.File != nil {
		if err := msg.File.PrepareForDecryption(); err != nil {
			return nil, fmt.Errorf("unable to decrypt: %w", err)
		}

		imageURI, err := msg.File.URL.Parse()
		if err != nil {
			return nil, fmt.Errorf("invalid URI: %v", err)
		}

		reader, err := im.parent.Client.Download(ctx, imageURI)
		if err != nil {
			return nil, fmt.Errorf("download failed: %w", err)
		}

		imageReader = msg.File.DecryptStream(reader)
	} else {
		imageURI, err := msg.URL.Parse()
		if err != nil {
			return nil, fmt.Errorf("invalid URI: %v", err)
		}

		reader, err := im.parent.Client.Download(ctx, imageURI)
		if err != nil {
			return nil, fmt.Errorf("download failed: %w", err)
		}

		imageReader = reader
	}

	img, _, err := image.Decode(imageReader)
	if err != nil {
		return nil, fmt.Errorf("decode failed: %w", err)
	}

	return img, nil
}

func (im *ImageMessage) Caption() (string, bool) {
	msg := im.evt.Content.AsMessage()
	if msg.MsgType != event.MsgImage {
		panic(fmt.Sprintf("expected msgtype of m.image, got %s", msg.MsgType))
	}

	// "If the filename is present, and its value is different than body, then
	// body is considered to be a caption, otherwise body is a filename. format
	// and formatted_body are only used for captions."
	//
	// https://spec.matrix.org/v1.10/client-server-api/#media-captions
	if msg.FileName != "" && msg.FileName != msg.Body {
		return msg.Body, true
	} else {
		return "", false
	}
}

func (im *ImageMessage) FileName() string {
	msg := im.evt.Content.AsMessage()
	if msg.MsgType != event.MsgImage {
		panic(fmt.Sprintf("expected msgtype of m.image, got %s", msg.MsgType))
	}

	filename := msg.FileName
	if filename == "" {
		filename = msg.Body
	}

	return filename
}

type Room struct {
	parent *Matrix
	ID     id.RoomID
}

func (r Room) Send(ctx context.Context, msg string) error {
	return r.sendMessageEvent(ctx, &event.MessageEventContent{
		MsgType: event.MsgText,
		Body:    msg,
	})
}

func (r Room) SendEmote(ctx context.Context, msg string) error {
	return r.sendMessageEvent(ctx, &event.MessageEventContent{
		MsgType: event.MsgEmote,
		Body:    msg,
	})
}

func (r Room) sendMessageEvent(ctx context.Context, evt *event.MessageEventContent) error {
	_, err := r.parent.Client.SendMessageEvent(ctx, r.ID, event.EventMessage, evt)
	return err
}

func (r Room) GetAccountData(ctx context.Context, name string, output any) error {
	return r.parent.Client.GetRoomAccountData(ctx, r.ID, name, output)
}

func (r Room) SetAccountData(ctx context.Context, name string, data any) error {
	return r.parent.Client.SetRoomAccountData(ctx, r.ID, name, data)
}

func (r Room) IsEncrypted(ctx context.Context) (bool, error) {
	return r.parent.Client.StateStore.IsEncrypted(ctx, r.ID)
}

func (r Room) EnableEncryption(ctx context.Context) error {
	_, err := r.parent.Client.SendStateEvent(ctx, r.ID,
		event.StateEncryption, "", event.EncryptionEventContent{
			Algorithm:              "m.megolm.v1.aes-sha2",
			RotationPeriodMillis:   604800000,
			RotationPeriodMessages: 100,
		})

	return err
}

func (r Room) StartTyping(ctx context.Context, timeout time.Duration) error {
	_, err := r.parent.Client.UserTyping(ctx, r.ID, true, timeout)
	return err
}

func (r Room) StopTyping(ctx context.Context) error {
	_, err := r.parent.Client.UserTyping(ctx, r.ID, false, 0)
	return err
}