package module

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"time"

	"github.com/google/jsonschema-go/jsonschema"
	"github.com/jwetzell/showbridge-go/internal/common"
	"github.com/jwetzell/showbridge-go/internal/config"
	"github.com/jwetzell/showbridge-go/internal/framer"
)

type TCPClient struct {
	config config.ModuleConfig
	framer framer.Framer
	conn   *net.TCPConn
	ctx    context.Context
	router common.RouteIO
	Addr   *net.TCPAddr
	logger *slog.Logger
	cancel context.CancelFunc
}

func init() {
	RegisterModule(ModuleRegistration{
		Type:  "net.tcp.client",
		Title: "TCP Client",
		ParamsSchema: &jsonschema.Schema{
			Type: "object",
			Properties: map[string]*jsonschema.Schema{
				"host": {
					Title: "Host",
					Type:  "string",
				},
				"port": {
					Title:   "Port",
					Type:    "integer",
					Minimum: jsonschema.Ptr[float64](1),
					Maximum: jsonschema.Ptr[float64](65535),
				},
				"framing": {
					Title: "Framing Method",
					Type:  "string",
					Enum:  []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
				},
			},
			Required:             []string{"host", "port", "framing"},
			AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
		},
		New: func(config config.ModuleConfig) (common.Module, error) {
			params := config.Params
			hostString, err := params.GetString("host")
			if err != nil {
				return nil, fmt.Errorf("net.tcp.client host error: %w", err)
			}

			portNum, err := params.GetInt("port")
			if err != nil {
				return nil, fmt.Errorf("net.tcp.client port error: %w", err)
			}

			addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
			if err != nil {
				return nil, err
			}

			framingMethodString, err := params.GetString("framing")
			if err != nil {
				return nil, fmt.Errorf("net.tcp.client framing error: %w", err)
			}

			framer := framer.GetFramer(framingMethodString)

			if framer == nil {
				return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethodString)
			}
			return &TCPClient{framer: framer, Addr: addr, config: config, logger: CreateLogger(config)}, nil
		},
	})
}

func (tc *TCPClient) Id() string {
	return tc.config.Id
}

func (tc *TCPClient) Type() string {
	return tc.config.Type
}

func (tc *TCPClient) Start(ctx context.Context, router common.RouteIO) error {
	tc.logger.Debug("running")
	tc.router = router
	moduleContext, cancel := context.WithCancel(ctx)
	tc.ctx = moduleContext
	tc.cancel = cancel

	// TODO(jwetzell): shutdown with router.Context properly
	go func() {
		<-tc.ctx.Done()
		tc.logger.Debug("done")
		if tc.conn != nil {
			tc.conn.Close()
		}
	}()

	for {
		err := tc.SetupConn()
		if err != nil {
			if tc.ctx.Err() != nil {
				tc.logger.Debug("done")
				return nil
			}
			tc.logger.Error("connection error", "error", err.Error())
			time.Sleep(time.Second * 2)
			continue
		}

		buffer := make([]byte, 1024)
		select {
		case <-tc.ctx.Done():
			tc.logger.Debug("done")
			return nil
		default:
		READ:
			for {
				select {
				case <-tc.ctx.Done():
					tc.logger.Debug("done")
					return nil
				default:
					byteCount, err := tc.conn.Read(buffer)

					if err != nil {
						tc.framer.Clear()
						break READ
					}

					if tc.framer != nil {
						if byteCount > 0 {
							messages := tc.framer.Decode(buffer[0:byteCount])
							for _, message := range messages {
								if tc.router != nil {
									tc.router.HandleInput(tc.ctx, tc.Id(), message)
								} else {
									tc.logger.Error("input received but no router is configured")
								}
							}
						}
					}
				}
			}
		}
	}
}

func (tc *TCPClient) SetupConn() error {
	client, err := net.DialTCP("tcp", nil, tc.Addr)
	tc.conn = client
	return err
}

func (tc *TCPClient) Output(ctx context.Context, payload any) error {
	// NOTE(jwetzell): not sure how this would occur but
	if tc.conn == nil {
		err := tc.SetupConn()
		if err != nil {
			return err
		}
	}
	payloadBytes, ok := common.GetAnyAsByteSlice(payload)
	if !ok {
		return errors.New("net.tcp.client is only able to output bytes")
	}
	_, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
	return err
}

func (tc *TCPClient) Stop() {
	tc.cancel()
}
