// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT

package v2

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	actions_model "code.gitea.io/gitea/models/actions"
	"code.gitea.io/gitea/models/db"
	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/modules/actions"
	"code.gitea.io/gitea/modules/json"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/services/context"
)

// RawMessage is a raw encoded JSON value (equivalent to encoding/json.RawMessage)
type RawMessage []byte

// MCP Protocol Types (JSON-RPC 2.0)

type MCPRequest struct {
	JSONRPC string     `json:"jsonrpc"`
	ID      any        `json:"id"`
	Method  string     `json:"method"`
	Params  RawMessage `json:"params,omitempty"`
}

type MCPResponse struct {
	JSONRPC string    `json:"jsonrpc"`
	ID      any       `json:"id"`
	Result  any       `json:"result,omitempty"`
	Error   *MCPError `json:"error,omitempty"`
}

type MCPError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    any    `json:"data,omitempty"`
}

// MCP Tool definitions
type MCPTool struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	InputSchema map[string]any `json:"inputSchema"`
}

type MCPToolsListResult struct {
	Tools []MCPTool `json:"tools"`
}

type MCPToolCallParams struct {
	Name      string         `json:"name"`
	Arguments map[string]any `json:"arguments"`
}

type MCPToolCallResult struct {
	Content []MCPContent `json:"content"`
	IsError bool         `json:"isError,omitempty"`
}

type MCPContent struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type MCPInitializeParams struct {
	ProtocolVersion string            `json:"protocolVersion"`
	Capabilities    map[string]any    `json:"capabilities"`
	ClientInfo      map[string]string `json:"clientInfo"`
}

type MCPInitializeResult struct {
	ProtocolVersion string            `json:"protocolVersion"`
	Capabilities    map[string]any    `json:"capabilities"`
	ServerInfo      map[string]string `json:"serverInfo"`
}

// Available MCP tools
var mcpTools = []MCPTool{
	{
		Name:        "list_runners",
		Description: "List all runners with their status, capabilities, and current workload",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner (optional, lists global runners if omitted)",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name (optional)",
				},
				"status": map[string]any{
					"type":        "string",
					"enum":        []string{"online", "offline", "all"},
					"description": "Filter by runner status",
				},
			},
		},
	},
	{
		Name:        "get_runner",
		Description: "Get detailed information about a specific runner including capabilities, disk space, and bandwidth",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"runner_id": map[string]any{
					"type":        "integer",
					"description": "The runner ID",
				},
			},
			"required": []string{"runner_id"},
		},
	},
	{
		Name:        "list_workflow_runs",
		Description: "List workflow runs for a repository with status and timing information",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name",
				},
				"status": map[string]any{
					"type":        "string",
					"enum":        []string{"pending", "running", "success", "failure", "cancelled", "all"},
					"description": "Filter by run status",
				},
				"limit": map[string]any{
					"type":        "integer",
					"description": "Maximum number of runs to return (default 20)",
				},
			},
			"required": []string{"owner", "repo"},
		},
	},
	{
		Name:        "get_workflow_run",
		Description: "Get detailed information about a specific workflow run including all jobs and their status",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name",
				},
				"run_id": map[string]any{
					"type":        "integer",
					"description": "The workflow run ID",
				},
			},
			"required": []string{"owner", "repo", "run_id"},
		},
	},
	{
		Name:        "get_job_logs",
		Description: "Get logs from a specific job in a workflow run",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name",
				},
				"job_id": map[string]any{
					"type":        "integer",
					"description": "The job ID",
				},
			},
			"required": []string{"owner", "repo", "job_id"},
		},
	},
	{
		Name:        "list_releases",
		Description: "List releases for a repository",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name",
				},
				"limit": map[string]any{
					"type":        "integer",
					"description": "Maximum number of releases to return (default 10)",
				},
			},
			"required": []string{"owner", "repo"},
		},
	},
	{
		Name:        "get_release",
		Description: "Get details of a specific release including all assets",
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"owner": map[string]any{
					"type":        "string",
					"description": "Repository owner",
				},
				"repo": map[string]any{
					"type":        "string",
					"description": "Repository name",
				},
				"tag": map[string]any{
					"type":        "string",
					"description": "Release tag (e.g., v1.0.0)",
				},
			},
			"required": []string{"owner", "repo", "tag"},
		},
	},
}

// MCPHandler handles MCP protocol requests
// @Summary MCP Protocol Endpoint
// @Description Handles Model Context Protocol requests for AI tool integration
// @Tags mcp
// @Accept json
// @Produce json
// @Success 200 {object} MCPResponse
// @Router /mcp [post]
func MCPHandler(ctx *context.APIContext) {
	body, err := io.ReadAll(ctx.Req.Body)
	if err != nil {
		sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
		return
	}

	var req MCPRequest
	if err := json.Unmarshal(body, &req); err != nil {
		sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
		return
	}

	if req.JSONRPC != "2.0" {
		sendMCPError(ctx, req.ID, -32600, "Invalid Request", "jsonrpc must be 2.0")
		return
	}

	log.Debug("MCP request: method=%s id=%v", req.Method, req.ID)

	switch req.Method {
	case "initialize":
		handleInitialize(ctx, &req)
	case "tools/list":
		handleToolsList(ctx, &req)
	case "tools/call":
		handleToolsCall(ctx, &req)
	case "ping":
		sendMCPResult(ctx, req.ID, map[string]string{})
	default:
		sendMCPError(ctx, req.ID, -32601, "Method not found", "Unknown method: "+req.Method)
	}
}

func handleInitialize(ctx *context.APIContext, req *MCPRequest) {
	result := MCPInitializeResult{
		ProtocolVersion: "2024-11-05",
		Capabilities: map[string]any{
			"tools": map[string]any{},
		},
		ServerInfo: map[string]string{
			"name":    "gitea-actions",
			"version": setting.AppVer,
		},
	}
	sendMCPResult(ctx, req.ID, result)
}

func handleToolsList(ctx *context.APIContext, req *MCPRequest) {
	result := MCPToolsListResult{Tools: mcpTools}
	sendMCPResult(ctx, req.ID, result)
}

func handleToolsCall(ctx *context.APIContext, req *MCPRequest) {
	var params MCPToolCallParams
	if err := json.Unmarshal(req.Params, &params); err != nil {
		sendMCPError(ctx, req.ID, -32602, "Invalid params", err.Error())
		return
	}

	var result any
	var err error

	switch params.Name {
	case "list_runners":
		result, err = toolListRunners(ctx, params.Arguments)
	case "get_runner":
		result, err = toolGetRunner(ctx, params.Arguments)
	case "list_workflow_runs":
		result, err = toolListWorkflowRuns(ctx, params.Arguments)
	case "get_workflow_run":
		result, err = toolGetWorkflowRun(ctx, params.Arguments)
	case "get_job_logs":
		result, err = toolGetJobLogs(ctx, params.Arguments)
	case "list_releases":
		result, err = toolListReleases(ctx, params.Arguments)
	case "get_release":
		result, err = toolGetRelease(ctx, params.Arguments)
	default:
		sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
		return
	}

	if err != nil {
		sendMCPToolResult(ctx, req.ID, err.Error(), true)
		return
	}

	// Convert result to JSON text
	jsonBytes, _ := json.MarshalIndent(result, "", "  ")
	sendMCPToolResult(ctx, req.ID, string(jsonBytes), false)
}

func sendMCPResult(ctx *context.APIContext, id, result any) {
	ctx.JSON(http.StatusOK, MCPResponse{
		JSONRPC: "2.0",
		ID:      id,
		Result:  result,
	})
}

func sendMCPError(ctx *context.APIContext, id any, code int, message, data string) {
	ctx.JSON(http.StatusOK, MCPResponse{
		JSONRPC: "2.0",
		ID:      id,
		Error: &MCPError{
			Code:    code,
			Message: message,
			Data:    data,
		},
	})
}

func sendMCPToolResult(ctx *context.APIContext, id any, text string, isError bool) {
	ctx.JSON(http.StatusOK, MCPResponse{
		JSONRPC: "2.0",
		ID:      id,
		Result: MCPToolCallResult{
			Content: []MCPContent{{Type: "text", Text: text}},
			IsError: isError,
		},
	})
}

// Tool implementations

func toolListRunners(ctx *context.APIContext, args map[string]any) (any, error) {
	var runners actions_model.RunnerList
	var err error

	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)

	if owner != "" && repo != "" {
		// Get repo-specific runners
		repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
		if err != nil {
			return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
		}
		runners, err = actions_model.GetRunnersOfRepo(ctx, repository.ID)
		if err != nil {
			return nil, err
		}
	} else {
		// Get all runners (admin)
		opts := actions_model.FindRunnerOptions{}
		opts.PageSize = 100
		runners, err = db.Find[actions_model.ActionRunner](ctx, opts)
		if err != nil {
			return nil, err
		}
	}

	statusFilter, _ := args["status"].(string)

	result := make([]map[string]any, 0, len(runners))
	for _, runner := range runners {
		isOnline := runner.IsOnline()

		if statusFilter == "online" && !isOnline {
			continue
		}
		if statusFilter == "offline" && isOnline {
			continue
		}

		r := map[string]any{
			"id":          runner.ID,
			"name":        runner.Name,
			"is_online":   isOnline,
			"status":      runner.Status().String(),
			"version":     runner.Version,
			"labels":      runner.AgentLabels,
			"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
		}

		// Parse capabilities if available
		if runner.CapabilitiesJSON != "" {
			var caps api.RunnerCapability
			if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
				r["capabilities"] = caps
			}
		}

		result = append(result, r)
	}

	return map[string]any{
		"runners": result,
		"count":   len(result),
	}, nil
}

func toolGetRunner(ctx *context.APIContext, args map[string]any) (any, error) {
	runnerIDFloat, ok := args["runner_id"].(float64)
	if !ok {
		return nil, errors.New("runner_id is required")
	}
	runnerID := int64(runnerIDFloat)

	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
	if err != nil {
		return nil, fmt.Errorf("runner not found: %d", runnerID)
	}

	result := map[string]any{
		"id":          runner.ID,
		"name":        runner.Name,
		"is_online":   runner.IsOnline(),
		"status":      runner.Status().String(),
		"version":     runner.Version,
		"labels":      runner.AgentLabels,
		"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
		"repo_id":     runner.RepoID,
		"owner_id":    runner.OwnerID,
	}

	if runner.CapabilitiesJSON != "" {
		var caps api.RunnerCapability
		if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
			result["capabilities"] = caps
		}
	}

	return result, nil
}

func toolListWorkflowRuns(ctx *context.APIContext, args map[string]any) (any, error) {
	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)

	if owner == "" || repo == "" {
		return nil, errors.New("owner and repo are required")
	}

	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
	if err != nil {
		return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
	}

	limit := 20
	if l, ok := args["limit"].(float64); ok {
		limit = int(l)
	}

	opts := actions_model.FindRunOptions{
		RepoID: repository.ID,
	}
	opts.PageSize = limit

	runs, err := db.Find[actions_model.ActionRun](ctx, opts)
	if err != nil {
		return nil, err
	}

	statusFilter, _ := args["status"].(string)

	result := make([]map[string]any, 0, len(runs))
	for _, run := range runs {
		status := run.Status.String()

		if statusFilter != "" && statusFilter != "all" && !strings.EqualFold(status, statusFilter) {
			continue
		}

		r := map[string]any{
			"id":          run.ID,
			"title":       run.Title,
			"status":      status,
			"event":       string(run.Event),
			"workflow_id": run.WorkflowID,
			"ref":         run.Ref,
			"commit_sha":  run.CommitSHA,
			"started":     run.Started.AsTime().Format(time.RFC3339),
			"stopped":     run.Stopped.AsTime().Format(time.RFC3339),
		}

		result = append(result, r)
	}

	return map[string]any{
		"runs":  result,
		"count": len(result),
		"repo":  fmt.Sprintf("%s/%s", owner, repo),
	}, nil
}

func toolGetWorkflowRun(ctx *context.APIContext, args map[string]any) (any, error) {
	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)
	runIDFloat, ok := args["run_id"].(float64)

	if owner == "" || repo == "" || !ok {
		return nil, errors.New("owner, repo, and run_id are required")
	}
	runID := int64(runIDFloat)

	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
	if err != nil {
		return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
	}

	run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, runID)
	if err != nil {
		return nil, fmt.Errorf("run not found: %d", runID)
	}

	// Get jobs for this run
	jobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
	if err != nil {
		return nil, err
	}

	jobResults := make([]map[string]any, 0, len(jobs))
	for _, job := range jobs {
		j := map[string]any{
			"id":      job.ID,
			"name":    job.Name,
			"status":  job.Status.String(),
			"started": job.Started.AsTime().Format(time.RFC3339),
			"stopped": job.Stopped.AsTime().Format(time.RFC3339),
			"task_id": job.TaskID,
		}
		jobResults = append(jobResults, j)
	}

	return map[string]any{
		"id":          run.ID,
		"title":       run.Title,
		"status":      run.Status.String(),
		"event":       string(run.Event),
		"workflow_id": run.WorkflowID,
		"ref":         run.Ref,
		"commit_sha":  run.CommitSHA,
		"started":     run.Started.AsTime().Format(time.RFC3339),
		"stopped":     run.Stopped.AsTime().Format(time.RFC3339),
		"jobs":        jobResults,
		"job_count":   len(jobResults),
	}, nil
}

func toolGetJobLogs(ctx *context.APIContext, args map[string]any) (any, error) {
	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)
	jobIDFloat, ok := args["job_id"].(float64)

	if owner == "" || repo == "" || !ok {
		return nil, errors.New("owner, repo, and job_id are required")
	}
	jobID := int64(jobIDFloat)

	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
	if err != nil {
		return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
	}

	job, err := actions_model.GetRunJobByID(ctx, jobID)
	if err != nil {
		return nil, fmt.Errorf("job not found: %d", jobID)
	}

	// Verify job belongs to this repo
	run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, job.RunID)
	if err != nil {
		return nil, errors.New("job not found in repository")
	}
	_ = run

	// Get the task for this job
	if job.TaskID == 0 {
		return map[string]any{
			"job_id":   jobID,
			"job_name": job.Name,
			"status":   job.Status.String(),
			"message":  "Job has not started yet - no task assigned",
		}, nil
	}

	task, err := actions_model.GetTaskByID(ctx, job.TaskID)
	if err != nil {
		return nil, fmt.Errorf("task not found for job: %d", jobID)
	}

	// Check if logs are expired
	if task.LogExpired {
		return map[string]any{
			"job_id":   jobID,
			"job_name": job.Name,
			"status":   job.Status.String(),
			"message":  "Logs have expired",
		}, nil
	}

	// Get steps for this task
	steps := actions.FullSteps(task)

	stepLogs := make([]map[string]any, 0, len(steps))
	for i, step := range steps {
		stepInfo := map[string]any{
			"step":     i,
			"name":     step.Name,
			"status":   step.Status.String(),
			"duration": step.Duration().String(),
		}

		// Read logs for this step
		if step.LogLength > 0 && step.LogIndex < int64(len(task.LogIndexes)) {
			offset := task.LogIndexes[step.LogIndex]
			logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, step.LogLength)
			if err != nil {
				stepInfo["error"] = fmt.Sprintf("failed to read logs: %v", err)
			} else {
				lines := make([]string, 0, len(logRows))
				for _, row := range logRows {
					lines = append(lines, row.Content)
				}
				stepInfo["lines"] = lines
				stepInfo["line_count"] = len(lines)
			}
		}

		stepLogs = append(stepLogs, stepInfo)
	}

	return map[string]any{
		"job_id":      jobID,
		"job_name":    job.Name,
		"status":      job.Status.String(),
		"task_id":     task.ID,
		"log_expired": task.LogExpired,
		"steps":       stepLogs,
		"step_count":  len(stepLogs),
	}, nil
}

func toolListReleases(ctx *context.APIContext, args map[string]any) (any, error) {
	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)

	if owner == "" || repo == "" {
		return nil, errors.New("owner and repo are required")
	}

	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
	if err != nil {
		return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
	}

	limit := 10
	if l, ok := args["limit"].(float64); ok {
		limit = int(l)
	}

	opts := repo_model.FindReleasesOptions{
		RepoID: repository.ID,
	}
	opts.PageSize = limit

	releases, err := db.Find[repo_model.Release](ctx, opts)
	if err != nil {
		return nil, err
	}

	result := make([]map[string]any, 0, len(releases))
	for _, release := range releases {
		r := map[string]any{
			"id":            release.ID,
			"tag_name":      release.TagName,
			"title":         release.Title,
			"is_draft":      release.IsDraft,
			"is_prerelease": release.IsPrerelease,
			"created_at":    release.CreatedUnix.AsTime().Format(time.RFC3339),
		}
		result = append(result, r)
	}

	return map[string]any{
		"releases": result,
		"count":    len(result),
		"repo":     fmt.Sprintf("%s/%s", owner, repo),
	}, nil
}

func toolGetRelease(ctx *context.APIContext, args map[string]any) (any, error) {
	owner, _ := args["owner"].(string)
	repo, _ := args["repo"].(string)
	tag, _ := args["tag"].(string)

	if owner == "" || repo == "" || tag == "" {
		return nil, errors.New("owner, repo, and tag are required")
	}

	repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
	if err != nil {
		return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
	}

	release, err := repo_model.GetRelease(ctx, repository.ID, tag)
	if err != nil {
		return nil, fmt.Errorf("release not found: %s", tag)
	}

	// Load attachments
	if err := release.LoadAttributes(ctx); err != nil {
		return nil, err
	}

	assets := make([]map[string]any, 0, len(release.Attachments))
	for _, att := range release.Attachments {
		assets = append(assets, map[string]any{
			"id":             att.ID,
			"name":           att.Name,
			"size":           att.Size,
			"download_count": att.DownloadCount,
			"download_url": fmt.Sprintf("%s/%s/%s/releases/download/%s/%s",
				setting.AppURL, owner, repo, tag, att.Name),
		})
	}

	return map[string]any{
		"id":            release.ID,
		"tag_name":      release.TagName,
		"title":         release.Title,
		"body":          release.Note,
		"is_draft":      release.IsDraft,
		"is_prerelease": release.IsPrerelease,
		"created_at":    release.CreatedUnix.AsTime().Format(time.RFC3339),
		"assets":        assets,
		"asset_count":   len(assets),
	}, nil
}
