package node

import (
	"context"
	"errors"
	"io/fs"
	"maps"

	"iop/apps/node/internal/transport"
	"iop/apps/node/internal/workspace"
	"iop/packages/go/workspaceprotocol"
	iop "iop/proto/gen/iop"
)

// OnWorkspaceOpen binds the immutable coordinator request identity to one
// already-approved catalog ref. It never exposes Node paths or catalog errors.
func (n *Node) OnWorkspaceOpen(_ context.Context, _ *transport.Session, req *iop.WorkspaceOpenRequest) (*iop.WorkspaceOpenResponse, error) {
	if req == nil {
		status, code := iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.OpenTerminal(status, code)
		return &iop.WorkspaceOpenResponse{Status: status, ErrorCode: code, Error: msg}, nil
	}
	response := &iop.WorkspaceOpenResponse{RequestId: req.GetRequestId(), WorkspaceRef: req.GetWorkspaceRef()}
	runtime := n.getWorkspaceRuntime()
	if runtime == nil {
		response.Status = iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED
		response.ErrorCode = iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY
		msg, _ := workspaceprotocol.OpenTerminal(response.Status, response.ErrorCode)
		response.Error = msg
		return response, nil
	}
	if _, err := runtime.Open(workspace.RequestAuthority{
		RequestID: req.GetRequestId(), WorkspaceRef: req.GetWorkspaceRef(),
		Operations:   append([]iop.WorkspaceOperation(nil), req.GetOperations()...),
		CommandIDs:   append([]string(nil), req.GetCommandIds()...),
		MaxReadBytes: req.GetMaxReadBytes(), MaxWriteBytes: req.GetMaxWriteBytes(),
		MaxOutputBytes: req.GetMaxOutputBytes(), MaxCommandTimeoutMS: req.GetMaxCommandTimeoutMs(),
	}); err != nil {
		applyOpenFailure(response, err)
		return response, nil
	}
	response.Status = iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS
	response.ErrorCode = iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSPECIFIED
	msg, _ := workspaceprotocol.OpenTerminal(response.Status, response.ErrorCode)
	response.Error = msg
	return response, nil
}

func (n *Node) OnWorkspaceTool(ctx context.Context, _ *transport.Session, req *iop.WorkspaceToolRequest) (*iop.WorkspaceToolResponse, error) {
	if req == nil {
		status, code := iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.ToolTerminal(status, code)
		return &iop.WorkspaceToolResponse{Status: status, ErrorCode: code, Error: msg}, nil
	}
	response := &iop.WorkspaceToolResponse{RequestId: req.GetRequestId(), StageId: req.GetStageId(), ToolCallId: req.GetToolCallId()}
	if req.GetRequestId() == "" || req.GetStageId() == "" || req.GetToolCallId() == "" {
		applyToolFailure(response, workspace.Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST})
		return response, nil
	}
	runtime := n.getWorkspaceRuntime()
	if runtime == nil {
		applyToolFailure(response, workspace.Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY})
		return response, nil
	}
	var result workspace.Result
	switch req.GetOperation() {
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_READ:
		if _, ok := req.GetInput().(*iop.WorkspaceToolRequest_RelativePath); !ok || len(req.GetEnvironment()) != 0 || req.GetTimeoutMs() != 0 {
			result = invalidToolResult()
		} else {
			result = runtime.Read(req.GetRequestId(), req.GetRelativePath())
		}
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST:
		if _, ok := req.GetInput().(*iop.WorkspaceToolRequest_RelativePath); !ok || len(req.GetEnvironment()) != 0 || req.GetTimeoutMs() != 0 {
			result = invalidToolResult()
		} else {
			result = runtime.List(req.GetRequestId(), req.GetRelativePath())
		}
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE:
		if _, ok := req.GetInput().(*iop.WorkspaceToolRequest_RelativePath); !ok || len(req.GetEnvironment()) != 0 || req.GetTimeoutMs() != 0 {
			result = invalidToolResult()
		} else {
			result = runtime.Delete(req.GetRequestId(), req.GetRelativePath())
		}
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE:
		write, ok := req.GetInput().(*iop.WorkspaceToolRequest_Write)
		if !ok || write.Write == nil || len(req.GetEnvironment()) != 0 || req.GetTimeoutMs() != 0 {
			result = invalidToolResult()
		} else {
			result = runtime.Write(req.GetRequestId(), write.Write.GetRelativePath(), write.Write.GetContent())
		}
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND:
		if _, ok := req.GetInput().(*iop.WorkspaceToolRequest_CommandId); !ok {
			result = invalidToolResult()
		} else {
			result = runtime.ExecuteCommand(ctx, workspace.CommandInput{
				RequestID: req.GetRequestId(), ToolCallID: req.GetToolCallId(),
				CommandID: req.GetCommandId(), Environment: maps.Clone(req.GetEnvironment()),
				TimeoutMS: req.GetTimeoutMs(),
			})
		}
	default:
		result = invalidToolResult()
	}
	applyToolFailure(response, result)
	return response, nil
}

// OnWorkspaceArtifact serves only the closed PLAN/REVIEW artifact family. Node
// alone maps those selectors to fixed filenames in the request-owned internal
// namespace; no artifact path is accepted from Edge or exposed to public tools.
func (n *Node) OnWorkspaceArtifact(_ context.Context, _ *transport.Session, req *iop.WorkspaceArtifactRequest) (*iop.WorkspaceArtifactResponse, error) {
	if req == nil {
		status, code := iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.ArtifactTerminal(status, code)
		return &iop.WorkspaceArtifactResponse{Status: status, ErrorCode: code, Error: msg}, nil
	}
	response := &iop.WorkspaceArtifactResponse{
		RequestId: req.GetRequestId(), Kind: req.GetKind(), Operation: req.GetOperation(),
	}
	name, kindOK := workspaceArtifactName(req.GetKind())
	operationOK := req.GetOperation() == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ ||
		req.GetOperation() == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE
	if req.GetRequestId() == "" || !kindOK || !operationOK ||
		(req.GetOperation() == iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ && len(req.GetContent()) != 0) {
		applyArtifactFailure(response, workspace.ErrInvalidRequest)
		return response, nil
	}
	runtime := n.getWorkspaceRuntime()
	if runtime == nil {
		applyArtifactFailure(response, workspace.ErrClosed)
		return response, nil
	}
	switch req.GetOperation() {
	case iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ:
		content, err := runtime.ReadInternalArtifact(req.GetRequestId(), name)
		if err != nil {
			applyArtifactFailure(response, err)
			return response, nil
		}
		response.Content = content
	case iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE:
		if err := runtime.WriteInternalArtifact(req.GetRequestId(), name, req.GetContent()); err != nil {
			applyArtifactFailure(response, err)
			return response, nil
		}
	}
	response.Status = iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS
	response.ErrorCode = iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSPECIFIED
	response.Error, _ = workspaceprotocol.ArtifactTerminal(response.Status, response.ErrorCode)
	return response, nil
}

func (n *Node) OnWorkspaceCancel(_ context.Context, _ *transport.Session, req *iop.WorkspaceCancelRequest) (*iop.WorkspaceCancelResponse, error) {
	if req == nil {
		status, code := iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.CancelTerminal(status, code)
		return &iop.WorkspaceCancelResponse{Status: status, ErrorCode: code, Error: msg}, nil
	}
	response := &iop.WorkspaceCancelResponse{RequestId: req.GetRequestId(), StageId: req.GetStageId(), ToolCallId: req.GetToolCallId()}
	if req.GetRequestId() == "" || req.GetStageId() == "" || req.GetToolCallId() == "" {
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.CancelTerminal(response.Status, response.ErrorCode)
		response.Error = msg
		return response, nil
	}
	runtime := n.getWorkspaceRuntime()
	if runtime == nil {
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY
		msg, _ := workspaceprotocol.CancelTerminal(response.Status, response.ErrorCode)
		response.Error = msg
		return response, nil
	}
	result := runtime.Cancel(req.GetRequestId(), req.GetToolCallId())
	response.Status, response.ErrorCode = result.Status, result.Code
	msg, ok := workspaceprotocol.CancelTerminal(result.Status, result.Code)
	if !ok {
		response.Status = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR
		response.ErrorCode = iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ = workspaceprotocol.CancelTerminal(response.Status, response.ErrorCode)
	}
	response.Error = msg
	return response, nil
}

func (n *Node) OnWorkspaceCleanup(ctx context.Context, _ *transport.Session, req *iop.WorkspaceCleanupRequest) (*iop.WorkspaceCleanupResponse, error) {
	if req == nil || req.GetRequestId() == "" {
		status, code := iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
		msg, _ := workspaceprotocol.CleanupTerminal(status, code)
		return &iop.WorkspaceCleanupResponse{Status: status, ErrorCode: code, Error: msg}, nil
	}
	response := &iop.WorkspaceCleanupResponse{RequestId: req.GetRequestId()}
	runtime := n.getWorkspaceRuntime()
	if runtime == nil {
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY
	} else {
		result := runtime.Cleanup(ctx, req.GetRequestId())
		response.Status, response.ErrorCode = result.Status, result.Code
		response.CleanedProcesses, response.CleanedArtifacts = result.CleanedProcesses, result.CleanedArtifacts
	}
	msg, ok := workspaceprotocol.CleanupTerminal(response.Status, response.ErrorCode)
	if !ok {
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL
		response.CleanedProcesses, response.CleanedArtifacts = 0, 0
		msg, _ = workspaceprotocol.CleanupTerminal(response.Status, response.ErrorCode)
	}
	response.Error = msg
	return response, nil
}

func invalidToolResult() workspace.Result {
	return workspace.Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST}
}

func applyOpenFailure(response *iop.WorkspaceOpenResponse, err error) {
	switch err {
	case workspace.ErrClosed:
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY
	default:
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
	}
	msg, _ := workspaceprotocol.OpenTerminal(response.Status, response.ErrorCode)
	response.Error = msg
}

func applyToolFailure(response *iop.WorkspaceToolResponse, result workspace.Result) {
	response.Status, response.ErrorCode = result.Status, result.Code
	response.Stdout, response.Stderr = result.Stdout, result.Stderr
	response.ExitCode, response.Truncated, response.DurationMs = result.ExitCode, result.Truncated, result.DurationMS
	if result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		response.Content, response.Entries, response.Truncated = result.Content, result.Entries, result.Truncated
		msg, _ := workspaceprotocol.ToolTerminal(result.Status, result.Code)
		response.Error = msg
		return
	}
	msg, ok := workspaceprotocol.ToolTerminal(result.Status, result.Code)
	if !ok {
		response.Status = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR
		response.ErrorCode = iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL
		msg, _ = workspaceprotocol.ToolTerminal(response.Status, response.ErrorCode)
	}
	response.Error = msg
}

func workspaceArtifactName(kind iop.WorkspaceArtifactKind) (string, bool) {
	switch kind {
	case iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN:
		return "plan.md", true
	case iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW:
		return "review.md", true
	default:
		return "", false
	}
}

func applyArtifactFailure(response *iop.WorkspaceArtifactResponse, err error) {
	switch {
	case errors.Is(err, workspace.ErrClosed):
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY
	case errors.Is(err, fs.ErrNotExist):
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND
	case errors.Is(err, workspace.ErrInvalidRequest), errors.Is(err, workspace.ErrRequestConflict), errors.Is(err, workspace.ErrUnknownWorkspace):
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST
	default:
		response.Status, response.ErrorCode = iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL
	}
	response.Content = nil
	response.Error, _ = workspaceprotocol.ArtifactTerminal(response.Status, response.ErrorCode)
}
