package coredeno

import (
	"context"
	"errors"
	"fmt"
	"strings"

	pb "forge.lthn.ai/core/go/pkg/coredeno/proto"
	"forge.lthn.ai/core/go/pkg/io"
	"forge.lthn.ai/core/go/pkg/manifest"
	"forge.lthn.ai/core/go/pkg/store"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// ProcessRunner abstracts process management for the gRPC server.
// Satisfied by *process.Service.
type ProcessRunner interface {
	Start(ctx context.Context, command string, args ...string) (ProcessHandle, error)
	Kill(id string) error
}

// ProcessHandle is returned by ProcessRunner.Start.
type ProcessHandle interface {
	Info() ProcessInfo
}

// ProcessInfo is the subset of process info the server needs.
type ProcessInfo struct {
	ID string
}

// Server implements the CoreService gRPC interface with permission gating.
// Every I/O request is checked against the calling module's declared permissions.
type Server struct {
	pb.UnimplementedCoreServiceServer
	medium    io.Medium
	store     *store.Store
	manifests map[string]*manifest.Manifest
	processes ProcessRunner
}

// NewServer creates a CoreService server backed by the given Medium and Store.
func NewServer(medium io.Medium, st *store.Store) *Server {
	return &Server{
		medium:    medium,
		store:     st,
		manifests: make(map[string]*manifest.Manifest),
	}
}

// RegisterModule adds a module's manifest to the permission registry.
func (s *Server) RegisterModule(m *manifest.Manifest) {
	s.manifests[m.Code] = m
}

// getManifest looks up a module and returns an error if unknown.
func (s *Server) getManifest(code string) (*manifest.Manifest, error) {
	m, ok := s.manifests[code]
	if !ok {
		return nil, fmt.Errorf("unknown module: %s", code)
	}
	return m, nil
}

// FileRead implements CoreService.FileRead with permission gating.
func (s *Server) FileRead(_ context.Context, req *pb.FileReadRequest) (*pb.FileReadResponse, error) {
	m, err := s.getManifest(req.ModuleCode)
	if err != nil {
		return nil, err
	}
	if !CheckPath(req.Path, m.Permissions.Read) {
		return nil, fmt.Errorf("permission denied: %s cannot read %s", req.ModuleCode, req.Path)
	}
	content, err := s.medium.Read(req.Path)
	if err != nil {
		return nil, err
	}
	return &pb.FileReadResponse{Content: content}, nil
}

// FileWrite implements CoreService.FileWrite with permission gating.
func (s *Server) FileWrite(_ context.Context, req *pb.FileWriteRequest) (*pb.FileWriteResponse, error) {
	m, err := s.getManifest(req.ModuleCode)
	if err != nil {
		return nil, err
	}
	if !CheckPath(req.Path, m.Permissions.Write) {
		return nil, fmt.Errorf("permission denied: %s cannot write %s", req.ModuleCode, req.Path)
	}
	if err := s.medium.Write(req.Path, req.Content); err != nil {
		return nil, err
	}
	return &pb.FileWriteResponse{Ok: true}, nil
}

// FileList implements CoreService.FileList with permission gating.
func (s *Server) FileList(_ context.Context, req *pb.FileListRequest) (*pb.FileListResponse, error) {
	m, err := s.getManifest(req.ModuleCode)
	if err != nil {
		return nil, err
	}
	if !CheckPath(req.Path, m.Permissions.Read) {
		return nil, fmt.Errorf("permission denied: %s cannot list %s", req.ModuleCode, req.Path)
	}
	entries, err := s.medium.List(req.Path)
	if err != nil {
		return nil, err
	}
	var pbEntries []*pb.FileEntry
	for _, e := range entries {
		info, _ := e.Info()
		pbEntries = append(pbEntries, &pb.FileEntry{
			Name:  e.Name(),
			IsDir: e.IsDir(),
			Size:  info.Size(),
		})
	}
	return &pb.FileListResponse{Entries: pbEntries}, nil
}

// FileDelete implements CoreService.FileDelete with permission gating.
func (s *Server) FileDelete(_ context.Context, req *pb.FileDeleteRequest) (*pb.FileDeleteResponse, error) {
	m, err := s.getManifest(req.ModuleCode)
	if err != nil {
		return nil, err
	}
	if !CheckPath(req.Path, m.Permissions.Write) {
		return nil, fmt.Errorf("permission denied: %s cannot delete %s", req.ModuleCode, req.Path)
	}
	if err := s.medium.Delete(req.Path); err != nil {
		return nil, err
	}
	return &pb.FileDeleteResponse{Ok: true}, nil
}

// storeGroupAllowed checks that the requested group is not a reserved system namespace.
// Groups prefixed with "_" are reserved for internal use (e.g. _coredeno, _modules).
// TODO: once the proto carries module_code on store requests, enforce per-module namespace isolation.
func storeGroupAllowed(group string) error {
	if strings.HasPrefix(group, "_") {
		return status.Errorf(codes.PermissionDenied, "reserved store group: %s", group)
	}
	return nil
}

// StoreGet implements CoreService.StoreGet with reserved namespace protection.
func (s *Server) StoreGet(_ context.Context, req *pb.StoreGetRequest) (*pb.StoreGetResponse, error) {
	if err := storeGroupAllowed(req.Group); err != nil {
		return nil, err
	}
	val, err := s.store.Get(req.Group, req.Key)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			return &pb.StoreGetResponse{Found: false}, nil
		}
		return nil, status.Errorf(codes.Internal, "store: %v", err)
	}
	return &pb.StoreGetResponse{Value: val, Found: true}, nil
}

// StoreSet implements CoreService.StoreSet with reserved namespace protection.
func (s *Server) StoreSet(_ context.Context, req *pb.StoreSetRequest) (*pb.StoreSetResponse, error) {
	if err := storeGroupAllowed(req.Group); err != nil {
		return nil, err
	}
	if err := s.store.Set(req.Group, req.Key, req.Value); err != nil {
		return nil, err
	}
	return &pb.StoreSetResponse{Ok: true}, nil
}

// SetProcessRunner sets the process runner for ProcessStart/ProcessStop.
func (s *Server) SetProcessRunner(pr ProcessRunner) {
	s.processes = pr
}

// ProcessStart implements CoreService.ProcessStart with permission gating.
func (s *Server) ProcessStart(ctx context.Context, req *pb.ProcessStartRequest) (*pb.ProcessStartResponse, error) {
	if s.processes == nil {
		return nil, status.Error(codes.Unimplemented, "process service not available")
	}
	m, err := s.getManifest(req.ModuleCode)
	if err != nil {
		return nil, err
	}
	if !CheckRun(req.Command, m.Permissions.Run) {
		return nil, fmt.Errorf("permission denied: %s cannot run %s", req.ModuleCode, req.Command)
	}
	proc, err := s.processes.Start(ctx, req.Command, req.Args...)
	if err != nil {
		return nil, fmt.Errorf("process start: %w", err)
	}
	return &pb.ProcessStartResponse{ProcessId: proc.Info().ID}, nil
}

// ProcessStop implements CoreService.ProcessStop.
func (s *Server) ProcessStop(_ context.Context, req *pb.ProcessStopRequest) (*pb.ProcessStopResponse, error) {
	if s.processes == nil {
		return nil, status.Error(codes.Unimplemented, "process service not available")
	}
	if err := s.processes.Kill(req.ProcessId); err != nil {
		return nil, fmt.Errorf("process stop: %w", err)
	}
	return &pb.ProcessStopResponse{Ok: true}, nil
}
