// +build !windows

package shim

import (
	"fmt"
	"os"
	"sync"
	"syscall"

	"github.com/containerd/console"
	shimapi "github.com/containerd/containerd/api/services/shim"
	"github.com/containerd/containerd/api/types/task"
	"github.com/containerd/containerd/reaper"
	google_protobuf "github.com/golang/protobuf/ptypes/empty"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
	"golang.org/x/sys/unix"
)

var empty = &google_protobuf.Empty{}

// New returns a new shim service that can be used via GRPC
func New(path string) *Service {
	return &Service{
		path:      path,
		processes: make(map[int]process),
		events:    make(chan *task.Event, 4096),
	}
}

type Service struct {
	initProcess *initProcess
	path        string
	id          string
	bundle      string
	mu          sync.Mutex
	processes   map[int]process
	events      chan *task.Event
	execID      int
}

func (s *Service) Create(ctx context.Context, r *shimapi.CreateRequest) (*shimapi.CreateResponse, error) {
	process, err := newInitProcess(ctx, s.path, r)
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	s.id = r.ID
	s.bundle = r.Bundle
	s.initProcess = process
	pid := process.Pid()
	s.processes[pid] = process
	s.mu.Unlock()
	cmd := &reaper.Cmd{
		ExitCh: make(chan int, 1),
	}
	reaper.Default.Register(pid, cmd)
	s.events <- &task.Event{
		Type: task.Event_CREATE,
		ID:   r.ID,
		Pid:  uint32(pid),
	}
	go s.waitExit(process, pid, cmd)
	return &shimapi.CreateResponse{
		Pid: uint32(pid),
	}, nil
}

func (s *Service) Start(ctx context.Context, r *shimapi.StartRequest) (*google_protobuf.Empty, error) {
	if err := s.initProcess.Start(ctx); err != nil {
		return nil, err
	}
	s.events <- &task.Event{
		Type: task.Event_START,
		ID:   s.id,
		Pid:  uint32(s.initProcess.Pid()),
	}
	return empty, nil
}

func (s *Service) Delete(ctx context.Context, r *shimapi.DeleteRequest) (*shimapi.DeleteResponse, error) {
	s.mu.Lock()
	p, ok := s.processes[int(r.Pid)]
	s.mu.Unlock()
	if !ok {
		p = s.initProcess
	}
	// TODO: how to handle errors here
	p.Delete(ctx)
	s.mu.Lock()
	delete(s.processes, p.Pid())
	s.mu.Unlock()
	return &shimapi.DeleteResponse{
		ExitStatus: uint32(p.Status()),
		ExitedAt:   p.ExitedAt(),
	}, nil
}

func (s *Service) Exec(ctx context.Context, r *shimapi.ExecRequest) (*shimapi.ExecResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.execID++

	process, err := newExecProcess(ctx, s.path, r, s.initProcess, s.execID)
	if err != nil {
		return nil, err
	}
	pid := process.Pid()
	cmd := &reaper.Cmd{
		ExitCh: make(chan int, 1),
	}
	reaper.Default.Register(pid, cmd)
	s.processes[pid] = process

	s.events <- &task.Event{
		Type: task.Event_EXEC_ADDED,
		ID:   s.id,
		Pid:  uint32(pid),
	}
	go s.waitExit(process, pid, cmd)
	return &shimapi.ExecResponse{
		Pid: uint32(pid),
	}, nil
}

func (s *Service) Pty(ctx context.Context, r *shimapi.PtyRequest) (*google_protobuf.Empty, error) {
	if r.Pid == 0 {
		return nil, errors.Errorf("pid not provided in request")
	}
	ws := console.WinSize{
		Width:  uint16(r.Width),
		Height: uint16(r.Height),
	}
	s.mu.Lock()
	p, ok := s.processes[int(r.Pid)]
	s.mu.Unlock()
	if !ok {
		return nil, errors.Errorf("process does not exist %d", r.Pid)
	}
	if err := p.Resize(ws); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) Events(r *shimapi.EventsRequest, stream shimapi.Shim_EventsServer) error {
	for e := range s.events {
		if err := stream.Send(e); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) State(ctx context.Context, r *shimapi.StateRequest) (*shimapi.StateResponse, error) {
	st, err := s.initProcess.ContainerStatus(ctx)
	if err != nil {
		return nil, err
	}
	status := task.StatusUnknown
	switch st {
	case "created":
		status = task.StatusCreated
	case "running":
		status = task.StatusRunning
	case "stopped":
		status = task.StatusStopped
	case "paused":
		status = task.StatusPaused
	}
	o := &shimapi.StateResponse{
		ID:        s.id,
		Bundle:    s.bundle,
		Pid:       uint32(s.initProcess.Pid()),
		Status:    status,
		Processes: []*task.Process{},
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, p := range s.processes {
		status := task.StatusRunning
		if err := unix.Kill(p.Pid(), 0); err != nil {
			if err != syscall.ESRCH {
				return nil, err
			}
			status = task.StatusStopped
		}
		o.Processes = append(o.Processes, &task.Process{
			Pid:    uint32(p.Pid()),
			Status: status,
		})
	}
	return o, nil
}

func (s *Service) Pause(ctx context.Context, r *shimapi.PauseRequest) (*google_protobuf.Empty, error) {
	if err := s.initProcess.Pause(ctx); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) Resume(ctx context.Context, r *shimapi.ResumeRequest) (*google_protobuf.Empty, error) {
	if err := s.initProcess.Resume(ctx); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) Exit(ctx context.Context, r *shimapi.ExitRequest) (*google_protobuf.Empty, error) {
	// signal ourself to exit
	if err := unix.Kill(os.Getpid(), syscall.SIGTERM); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) Kill(ctx context.Context, r *shimapi.KillRequest) (*google_protobuf.Empty, error) {
	if r.Pid == 0 {
		if err := s.initProcess.Kill(ctx, r.Signal, r.All); err != nil {
			return nil, err
		}
		return empty, nil
	}

	pids, err := s.getContainerPids(ctx, s.initProcess.id)
	if err != nil {
		return nil, err
	}

	valid := false
	for _, p := range pids {
		if r.Pid == p {
			valid = true
			break
		}
	}

	if !valid {
		return nil, errors.Errorf("process %d does not exist in container", r.Pid)
	}

	if err := unix.Kill(int(r.Pid), syscall.Signal(r.Signal)); err != nil {
		return nil, err
	}

	return empty, nil
}

func (s *Service) Processes(ctx context.Context, r *shimapi.ProcessesRequest) (*shimapi.ProcessesResponse, error) {
	pids, err := s.getContainerPids(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	ps := []*task.Process{}
	for _, pid := range pids {
		ps = append(ps, &task.Process{
			Pid: pid,
		})
	}
	resp := &shimapi.ProcessesResponse{
		Processes: ps,
	}

	return resp, nil

}

func (s *Service) CloseStdin(ctx context.Context, r *shimapi.CloseStdinRequest) (*google_protobuf.Empty, error) {
	p, ok := s.processes[int(r.Pid)]
	if !ok {
		return nil, fmt.Errorf("process does not exist %d", r.Pid)
	}
	if err := p.Stdin().Close(); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) Checkpoint(ctx context.Context, r *shimapi.CheckpointRequest) (*google_protobuf.Empty, error) {
	if err := s.initProcess.Checkpoint(ctx, r); err != nil {
		return nil, err
	}
	return empty, nil
}

func (s *Service) waitExit(p process, pid int, cmd *reaper.Cmd) {
	status := <-cmd.ExitCh
	p.Exited(status)

	reaper.Default.Delete(pid)
	s.events <- &task.Event{
		Type:       task.Event_EXIT,
		ID:         s.id,
		Pid:        uint32(pid),
		ExitStatus: uint32(status),
		ExitedAt:   p.ExitedAt(),
	}
}

func (s *Service) getContainerPids(ctx context.Context, id string) ([]uint32, error) {
	p, err := s.initProcess.runc.Ps(ctx, id)
	if err != nil {
		return nil, err
	}

	pids := make([]uint32, 0, len(p))
	for _, pid := range p {
		pids = append(pids, uint32(pid))
	}

	return pids, nil
}
