package app

import (
	"context"
	"fmt"
	"strings"

	"cyberstrike-ai/internal/agent"
	"cyberstrike-ai/internal/authctx"
	"cyberstrike-ai/internal/database"
	"cyberstrike-ai/internal/mcp"
	"cyberstrike-ai/internal/mcp/builtin"
)

func mcpToolAuthorizer(db *database.DB) func(context.Context, string, map[string]interface{}) error {
	return func(ctx context.Context, toolName string, args map[string]interface{}) error {
		principal, ok := authctx.PrincipalFromContext(ctx)
		if !ok {
			return fmt.Errorf("missing authenticated principal")
		}
		require := func(permission string) error {
			if !principal.HasPermission(permission) {
				return fmt.Errorf("missing permission %s", permission)
			}
			return nil
		}
		resource := func(permission, resourceType, argument string) error {
			if err := require(permission); err != nil {
				return err
			}
			id := mcpAuthorizationString(args, argument)
			if id == "" || db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), resourceType, id) {
				return fmt.Errorf("no access to %s %s", resourceType, id)
			}
			return nil
		}

		switch toolName {
		case builtin.ToolWebshellExec, builtin.ToolWebshellFileWrite:
			return resource("webshell:write", "webshell", "connection_id")
		case builtin.ToolWebshellFileList, builtin.ToolWebshellFileRead:
			return resource("webshell:read", "webshell", "connection_id")
		case builtin.ToolManageWebshellList:
			return require("webshell:read")
		case builtin.ToolManageWebshellAdd:
			return require("webshell:write")
		case builtin.ToolManageWebshellUpdate, builtin.ToolManageWebshellTest:
			return resource("webshell:write", "webshell", "connection_id")
		case builtin.ToolManageWebshellDelete:
			return resource("webshell:delete", "webshell", "connection_id")
		case builtin.ToolRecordVulnerability:
			if err := require("vulnerability:write"); err != nil {
				return err
			}
			conversationID := mcpAuthorizationString(args, "conversation_id")
			if conversationID == "" {
				conversationID = mcpAuthorizationConversationID(ctx)
			}
			if conversationID == "" || db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor("vulnerability:write"), "conversation", conversationID) {
				return fmt.Errorf("no access to conversation %s", conversationID)
			}
			return nil
		case builtin.ToolListVulnerabilities:
			if err := require("vulnerability:read"); err != nil {
				return err
			}
			conversationID := mcpAuthorizationConversationID(ctx)
			if conversationID == "" || db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor("vulnerability:read"), "conversation", conversationID) {
				return fmt.Errorf("no access to conversation %s", conversationID)
			}
			return nil
		case builtin.ToolGetVulnerability:
			return resource("vulnerability:read", "vulnerability", "id")
		case builtin.ToolUpsertProjectFact, builtin.ToolDeprecateProjectFact, builtin.ToolRestoreProjectFact:
			return authorizeProjectTool(ctx, principal, db, "project:write")
		case builtin.ToolGetProjectFact, builtin.ToolListProjectFacts, builtin.ToolSearchProjectFacts:
			return authorizeProjectTool(ctx, principal, db, "project:read")
		case builtin.ToolListKnowledgeRiskTypes, builtin.ToolSearchKnowledgeBase:
			return require("knowledge:read")
		case builtin.ToolAnalyzeImage:
			return require("agent:execute")
		case builtin.ToolBatchTaskList:
			return require("tasks:read")
		case builtin.ToolBatchTaskGet:
			return resource("tasks:read", "batch_task", "queue_id")
		case builtin.ToolBatchTaskCreate:
			if err := require("tasks:write"); err != nil {
				return err
			}
			if projectID := mcpAuthorizationString(args, "project_id"); projectID != "" && (db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor("tasks:write"), "project", projectID)) {
				return fmt.Errorf("no access to project %s", projectID)
			}
			return nil
		case builtin.ToolBatchTaskDelete, builtin.ToolBatchTaskRemove:
			return resource("tasks:delete", "batch_task", "queue_id")
		case builtin.ToolBatchTaskStart, builtin.ToolBatchTaskRerun, builtin.ToolBatchTaskPause,
			builtin.ToolBatchTaskUpdateMetadata, builtin.ToolBatchTaskUpdateSchedule,
			builtin.ToolBatchTaskScheduleEnabled, builtin.ToolBatchTaskAdd, builtin.ToolBatchTaskUpdate:
			return resource("tasks:write", "batch_task", "queue_id")
		case builtin.ToolC2Listener:
			return authorizeC2Action(principal, db, args, "c2_listener", "listener_id")
		case builtin.ToolC2Session, builtin.ToolC2Task, builtin.ToolC2File:
			if toolName == builtin.ToolC2File && mcpAuthorizationString(args, "action") == "get_result" {
				return authorizeC2Action(principal, db, args, "c2_task", "task_id")
			}
			return authorizeC2Action(principal, db, args, "c2_session", "session_id")
		case builtin.ToolC2TaskManage:
			return authorizeC2Action(principal, db, args, "c2_task", "task_id")
		case builtin.ToolC2Payload:
			return resource("c2:write", "c2_listener", "listener_id")
		case builtin.ToolC2Event:
			if id := mcpAuthorizationString(args, "session_id"); id != "" {
				return resource("c2:read", "c2_session", "session_id")
			}
			if principal.ScopeFor("c2:read") != database.RBACScopeAll {
				return fmt.Errorf("unfiltered C2 event list requires global scope")
			}
			return require("c2:read")
		case builtin.ToolC2Profile:
			// Profiles are process-global and do not yet have an owner. Writes are
			// therefore reserved for global scope; reads require c2:read.
			if mcpAuthorizationString(args, "action") == "list" || mcpAuthorizationString(args, "action") == "get" {
				return require("c2:read")
			}
			permission := "c2:write"
			if mcpAuthorizationString(args, "action") == "delete" {
				permission = "c2:delete"
			}
			if principal.ScopeFor(permission) != database.RBACScopeAll {
				return fmt.Errorf("C2 profile mutation requires global scope")
			}
			if mcpAuthorizationString(args, "action") == "delete" {
				return require("c2:delete")
			}
			return require("c2:write")
		default:
			if builtin.IsBuiltinTool(toolName) {
				return fmt.Errorf("no authorization policy registered for builtin tool %s", toolName)
			}
			if principal.HasPermission("agent:local-execute") {
				return nil
			}
			return fmt.Errorf("missing agent:local-execute")
		}
	}
}

func externalMCPToolAuthorizer() func(context.Context, string, map[string]interface{}) error {
	return func(ctx context.Context, toolName string, _ map[string]interface{}) error {
		principal, ok := authctx.PrincipalFromContext(ctx)
		if !ok {
			return fmt.Errorf("missing authenticated principal")
		}
		if !principal.HasPermission("mcp:external:execute") {
			return fmt.Errorf("missing permission mcp:external:execute")
		}
		if principal.ScopeFor("mcp:external:execute") != database.RBACScopeAll {
			return fmt.Errorf("external MCP invocation requires global scope")
		}
		if strings.TrimSpace(toolName) == "" {
			return fmt.Errorf("missing external tool name")
		}
		return nil
	}
}

func authorizeC2Action(principal authctx.Principal, db *database.DB, args map[string]interface{}, resourceType, argument string) error {
	action := mcpAuthorizationString(args, "action")
	permission := "c2:write"
	if action == "list" || action == "get" || action == "get_result" || action == "wait" {
		permission = "c2:read"
	} else if action == "delete" || action == "delete_batch" {
		permission = "c2:delete"
	}
	if !principal.HasPermission(permission) {
		return fmt.Errorf("missing permission %s", permission)
	}
	id := mcpAuthorizationString(args, argument)
	if action == "delete_batch" {
		ids := mcpAuthorizationStrings(args, argument+"s")
		if len(ids) == 0 {
			return fmt.Errorf("missing resource identifiers %ss", argument)
		}
		for _, candidate := range ids {
			if db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), resourceType, candidate) {
				return fmt.Errorf("no access to %s %s", resourceType, candidate)
			}
		}
		return nil
	}
	if id == "" {
		if action == "create" || action == "list" {
			return nil
		}
		return fmt.Errorf("missing resource identifier %s", argument)
	}
	if db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), resourceType, id) {
		return fmt.Errorf("no access to %s %s", resourceType, id)
	}
	return nil
}

func mcpAuthorizationStrings(args map[string]interface{}, key string) []string {
	values := []string{}
	switch raw := args[key].(type) {
	case []string:
		for _, value := range raw {
			if value = strings.TrimSpace(value); value != "" {
				values = append(values, value)
			}
		}
	case []interface{}:
		for _, item := range raw {
			if value, ok := item.(string); ok {
				if value = strings.TrimSpace(value); value != "" {
					values = append(values, value)
				}
			}
		}
	}
	return values
}

func authorizeProjectTool(ctx context.Context, principal authctx.Principal, db *database.DB, permission string) error {
	if !principal.HasPermission(permission) {
		return fmt.Errorf("missing permission %s", permission)
	}
	conversationID := mcpAuthorizationConversationID(ctx)
	if conversationID == "" || db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), "conversation", conversationID) {
		return fmt.Errorf("no access to conversation %s", conversationID)
	}
	projectID, err := db.GetConversationProjectID(conversationID)
	if err != nil || strings.TrimSpace(projectID) == "" || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), "project", projectID) {
		return fmt.Errorf("no access to project %s", projectID)
	}
	return nil
}

func mcpAuthorizationConversationID(ctx context.Context) string {
	if id := strings.TrimSpace(agent.ConversationIDFromContext(ctx)); id != "" {
		return id
	}
	return strings.TrimSpace(mcp.MCPConversationIDFromContext(ctx))
}

func mcpAuthorizationString(args map[string]interface{}, key string) string {
	value, _ := args[key].(string)
	return strings.TrimSpace(value)
}
