package phpx

import (
	"context"
	"encoding/json"
	"io"
	"os"
	"strings"
	"sync"

	_ "embed"

	"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
	"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
	"github.com/tkw1536/goprogram/lib/collection"
	"github.com/tkw1536/goprogram/lib/nobufio"
	"github.com/tkw1536/goprogram/stream"
)

// Server represents a server that executes PHP code.
// A typical use-case is to define functions using [MarshalEval], and then call those functions [MarshalCall].
//
// A server, once used, should be closed using the [Close] method.
type Server struct {
	// Context to use for the server
	Context context.Context

	// Executor is the executor used by this server.
	// It may not be modified concurrently with other processes.
	Executor Executor

	// prepares the server
	init sync.Once
	err  lazy.Lazy[error]

	// input / output for underlying executor
	in  io.WriteCloser
	out io.Reader

	m sync.Mutex // prevents concurrent access on any of the methods

	cancel context.CancelFunc
	c      context.Context // closed when server is finished
}

func (server *Server) prepare() error {
	server.init.Do(func() {
		// create input and output pipes
		ir, iw, err := os.Pipe()
		if err != nil {
			server.err.Set(ServerError{errInit, err})
			return
		}
		or, ow, err := os.Pipe()
		if err != nil {
			ir.Close()
			iw.Close()

			server.err.Set(ServerError{errInit, err})
			return
		}

		// create a context to close the server
		context, cancel := context.WithCancel(server.Context)
		server.cancel = cancel

		// start the shell process, which will close everything once done
		go func() {
			defer func() {
				ir.Close()
				iw.Close()
				or.Close()
				ow.Close()

				server.cancel()
			}()

			// start the server
			io := stream.NewIOStream(ow, nil, ir, 0)
			err := server.Executor.Spawn(server.c, io, serverPHP)
			server.err.Set(ServerError{errClosed, err})
		}()

		server.in = iw
		server.out = or
		server.c = context
	})

	return server.err.Get(nil)
}

// MarshalEval evaluates code on the server and Marshals the result into value.
// When value is nil, the results are discarded.
//
// code is directly passed to php's "eval" function.
// as such any functions defined will remain in server memory.
//
// When an exception is thrown by the PHP Code, error is not nil, and dest remains unchanged.
func (server *Server) MarshalEval(ctx context.Context, value any, code string) error {
	if err := server.prepare(); err != nil {
		return err
	}

	// input to be sent to the server
	delim := findDelimiter(code)
	input := delim + "\n" + code + "\n" + delim + "\n"

	server.m.Lock()
	defer server.m.Unlock()

	// when the server is already done
	if err := server.c.Err(); err != nil {
		return ServerError{Message: errClosed}
	}

	// find a delimiter for the code, and then send
	io.WriteString(server.in, input)

	data, err, _ := cancel.WithContext2(ctx, func(start func()) (string, error) {
		return nobufio.ReadLine(server.out)
	}, func() {
		server.cancel()
	})
	if err != nil {
		return ServerError{Message: errReceive, Err: err}
	}

	// read whatever we received
	var received [2]json.RawMessage
	if err := json.Unmarshal([]byte(data), &received); err != nil {
		return ServerError{Message: errReceive, Err: err}
	}

	// check if there was an error
	var errString string
	if err := json.Unmarshal(received[1], &errString); err == nil && errString != "" {
		return Throwable(errString)
	}

	// special case: no return value => no unmarshaling needed
	if value == nil {
		return nil
	}

	// read the actual result!
	return json.Unmarshal(received[0], value)
}

// Eval is like [MarshalEval], but returns the value as an any
func (server *Server) Eval(ctx context.Context, code string) (value any, err error) {
	err = server.MarshalEval(ctx, &value, code)
	return
}

// MarshalCall calls a previously defined function with the given arguments.
// Arguments are sent to php using json Marshal, and are 'json_decode'd on the php side.
//
// Return values are received as in [MarshalEval].
func (server *Server) MarshalCall(ctx context.Context, value any, function string, args ...any) error {
	// name of function to call
	name := MarshalString(function)

	// generate code to call
	var code string
	switch len(args) {
	case 0:
		code = "return call_user_func(" + name + ");"
	case 1:
		param, err := Marshal(args[0])
		if err != nil {
			return err
		}
		code = "return call_user_func(" + name + "," + param + ");"
	default:
		params, err := Marshal(args)
		if err != nil {
			return err
		}
		code = "return call_user_func_array(" + name + "," + params + ");"
	}

	// and evaluate the code
	return server.MarshalEval(ctx, value, code)
}

// Call is like [MarshalCall] but returns the return value of the function as an any
func (server *Server) Call(ctx context.Context, function string, args ...any) (value any, err error) {
	err = server.MarshalCall(ctx, &value, function, args...)
	return
}

const delimiterRune = 'F' // press to pay respect

// findDelimiter finds a delimiter that does not occur in the input string
func findDelimiter(input string) string {
	// find the longest sequence of delimiter rune
	var current, longest int
	for _, r := range input {
		if r == delimiterRune {
			current++
		} else {
			current = 0
		}

		if current > longest {
			longest = current
		}
	}
	// and then return it multipled longer than that
	return strings.Repeat(string(delimiterRune), longest+1)
}

// Close closes this server and prevents any further code from being run.
func (server *Server) Close() error {
	server.prepare()

	server.m.Lock()
	defer server.m.Unlock()

	// if the context is already closed
	if err := server.c.Err(); err != nil {
		return ServerError{Message: errClosed}
	}

	server.in.Close()
	<-server.c.Done()

	return nil
}

//go:embed server.php
var serverPHP string

// pre-process the server.php code to make it shorter
func init() {
	// remove the first '<?php' line
	lines := strings.Split(serverPHP, "\n")[1:]
	for i, line := range lines {
		lines[i] = strings.TrimSpace(line)
	}

	// remove comment lines
	lines = collection.Filter(lines, func(line string) bool {
		return !strings.HasPrefix(line, "//")
	})

	serverPHP = strings.Join(lines, "")
}
