package console

import (
	"agent/commons/debug"
	autils "agent/commons/utils"
	"agent/commons/utime"
	"agent/modules/console/defines/merrs"
	"agent/modules/console/internal/include"
	"agent/modules/console/internal/utils"
	"context"
	"io"
	"os/exec"
	"strings"
	"sync"

	"github.com/google/uuid"
)

type CloseProto func(uuid uuid.UUID)

type CommandItem = include.CommandItem

type NewConsoleOpts struct {
	Args          []string
	Cwd           string
	Env           []string
	CloseCallback CloseProto
	Utf8OnStart   bool
}

type Console struct {
	uuid          uuid.UUID
	closeCallback CloseProto

	command string
	args    []string
	cwd     string
	env     []string

	proc    *exec.Cmd
	inPipe  io.WriteCloser
	outPipe io.ReadCloser

	consoleOutput       chan []byte
	consoleCommands     chan CommandItem
	consoleInitCommands []CommandItem

	procCtx   context.Context
	procClose context.CancelFunc
	lock      *sync.RWMutex

	outputLines []string
	lastRawLine string
}

func (c *Console) Uuid() uuid.UUID {
	return c.uuid
}

func (c *Console) Start() error {
	return c.StartWithContext(context.Background())
}

func (c *Console) StartWithContext(ctx context.Context) (err error) {
	c.lock.Lock()
	defer c.lock.Unlock()

	if c.IsStarted() {
		return nil
	}

	c.procCtx, c.procClose = context.WithCancel(ctx)

	if c.closeCallback != nil {
		go func() {
			<-c.procCtx.Done()
			c.closeCallback(c.uuid)
		}()
	}

	proc := exec.CommandContext(c.procCtx, c.command, c.args...)
	proc.Env = c.env
	proc.Dir = c.cwd

	if c.inPipe, err = proc.StdinPipe(); err != nil {
		return err
	}
	if c.outPipe, err = proc.StdoutPipe(); err != nil {
		return err
	}
	proc.Stderr = proc.Stdout

	if err := proc.Start(); err != nil {
		return err
	}
	c.proc = proc

	debug.Logf("Process PID: %d", proc.Process.Pid)

	go c.pipeReader()
	go c.pipeWriter()
	go c.consumeOutput()

	return nil
}

func (c *Console) cancel(close bool) error {
	if close {
		c.Close()
	} else {
		pid := int64(c.proc.Process.Pid)
		utils.KillChildProcess(pid)
	}
	return nil
}

func (c *Console) AddCommand(ctx context.Context, command CommandItem) (err error) {
	if command.Command[0] == 0x03 || command.Command == "^C" {
		// При получении ^C пробуем завершить запущенные консолью команды
		c.consoleOutput <- []byte("> ^C\n")
		// Даем возможность консоли вывести сообщение
		utime.Sleep(1 * utime.Second)
		return c.cancel(false)
	} else if command.Command[0] == 0x1a || command.Command == "^Z" {
		// При получении ^Z закрываем консоль и пробуем завершить запущенные команды
		c.consoleOutput <- []byte("> ^Z\n")
		// Даем возможность консоли вывести сообщение
		utime.Sleep(1 * utime.Second)
		return c.cancel(true)
	}
	if c.IsStarted() && c.IsActive() {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case c.consoleCommands <- command:
			return err
		}
	}
	return merrs.NewConsoleIsntStartedError()
}

func (c *Console) PopOutput() (res []string) {
	c.lock.Lock()
	defer c.lock.Unlock()

	res = c.outputLines
	c.outputLines = nil

	return res
}

func (c *Console) IsActive() bool {
	select {
	case <-c.procCtx.Done():
		return false
	default:
		return true
	}
}

func (c *Console) IsStarted() bool {
	return c.procCtx != nil
}

func (c *Console) IsClosed() bool {
	return c.IsStarted() && !c.IsActive()
}

func (c *Console) Wait() error {
	<-c.procCtx.Done()
	return c.procCtx.Err()
}

func (c *Console) WaitChan() <-chan struct{} {
	return c.procCtx.Done()
}

func (c *Console) Close() {
	pid := int64(c.proc.Process.Pid)
	utils.KillChildProcess(pid)
	c.procClose()
	utils.KillSingleProcess(pid)
}

func (c *Console) pipeReader() {
	buff := make([]byte, 4096)
	for {
		n, err := c.outPipe.Read(buff)
		if err != nil {
			if err == io.EOF {
				return
			}
			debug.Log(err)
			return
		}
		select {
		case <-c.procCtx.Done():
			return
		case c.consoleOutput <- autils.SliceClone(buff[:n]):
		}
	}
}

func (c *Console) consumeOutput() {
	for {
		select {
		case <-c.procCtx.Done():
			return
		case data := <-c.consoleOutput:
			c.addOutput(data)
		}
	}
}

func (c *Console) addOutput(data []byte) {
	c.lock.Lock()
	defer c.lock.Unlock()

	if len(data) == 0 {
		return
	}

	// Старая строка является началом нового набора
	raw := c.lastRawLine + string(data)
	// Убираем лишний перевод строк
	raw = strings.ReplaceAll(raw, "\f\r", "")
	// Убираем все переносы каретки из строки, для нас они лишние.
	raw = strings.ReplaceAll(raw, "\r", "")

	items := strings.Split(raw, "\n")
	size := len(items)

	// Помещаем последнюю строку в необработанные, она являются началом первой строки из следующего набора
	c.lastRawLine = items[size-1]

	// Результат содержит только часть строки
	if size == 1 {
		// Ожидаем получения всей строки
		return
	}

	for _, line := range items[:size-1] {
		// При разделении строк был убран перенос "\n" - возвращаем его. На стороне сервиса,
		// строки объединяются следующщим образом strings.Join(lines, ""), т.е. без добавления к
		// ним "\n". Так сделано потому, что сервис не может знать, пришла ли к нему полная
		// строка с "\n" или только ее часть.
		c.outputLines = append(c.outputLines, line+"\n")
	}
}
