package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
)

type Run struct {
	ID            string  `json:"id"`
	WorkflowID    *string `json:"workflow_id,omitempty"`
	IssueID       *string `json:"issue_id,omitempty"`
	CardID        *string `json:"card_id,omitempty"`
	KanbanApp     *string `json:"kanban_app,omitempty"`
	Mode          string  `json:"mode"`
	Branch        string  `json:"branch"`
	WorktreePath  string  `json:"worktree_path"`
	Status        string  `json:"status"`
	StartedAt     int64   `json:"started_at"`
	FinishedAt    *int64  `json:"finished_at,omitempty"`
	AgentPID      *int    `json:"agent_pid,omitempty"`
	AgentLogPath  *string `json:"agent_log_path,omitempty"`
	Error         *string `json:"error,omitempty"`
}

type createRunRequest struct {
	WorkflowID string `json:"workflow_id"`
	IssueID    string `json:"issue_id"`
	CardID     string `json:"card_id"`
	KanbanApp  string `json:"kanban_app"`
	Mode       string `json:"mode"`
	Prompt     string `json:"prompt"`
}

type createRunResponse struct {
	RunID        string `json:"run_id"`
	Branch       string `json:"branch"`
	WorktreePath string `json:"worktree_path"`
	SSEURL       string `json:"sse_url"`
}

func writeJSON(w http.ResponseWriter, status int, body interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(body)
}

func writeErr(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}

func (a *App) handleHealth(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]interface{}{
		"status": "ok",
		"port":   a.cfg.Port,
		"db":     a.cfg.DBPath,
	})
}

// POST /api/runs
func (a *App) handleCreateRun(w http.ResponseWriter, r *http.Request) {
	var req createRunRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "invalid json: "+err.Error())
		return
	}
	if req.Mode == "" {
		req.Mode = "agent"
	}

	runID := "run_" + uuid.New().String()[:12]
	slug := req.IssueID
	if slug == "" {
		slug = req.CardID
	}
	if slug == "" {
		slug = runID
	}
	branch := fmt.Sprintf("auto/%s", slug)
	worktreePath := filepath.Join(a.cfg.WorktreesRoot, "wt-"+slug+"-"+runID[4:])
	now := time.Now().Unix()

	// Insert pending row
	_, err := a.db.Exec(`INSERT INTO runs
		(id, workflow_id, issue_id, card_id, kanban_app, mode, branch, worktree_path, status, started_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`,
		runID,
		nullStrFromS(req.WorkflowID),
		nullStrFromS(req.IssueID),
		nullStrFromS(req.CardID),
		nullStrFromS(req.KanbanApp),
		req.Mode, branch, worktreePath, now)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "insert run: "+err.Error())
		return
	}

	// Spawn async (worktree create blocks briefly but is short)
	prompt := req.Prompt
	if prompt == "" {
		prompt = fmt.Sprintf("Resolve %s in branch %s", slug, branch)
	}
	logPath := filepath.Join(worktreePath, "agent.log")

	res := Spawn(SpawnConfig{
		RepoRoot:     a.cfg.RepoRoot,
		Branch:       branch,
		WorktreePath: worktreePath,
		Prompt:       prompt,
		LogPath:      logPath,
	})

	if res.Error != "" {
		_, _ = a.db.Exec(`UPDATE runs SET status = 'failed', error = ?, finished_at = ?
			WHERE id = ?`, res.Error, time.Now().Unix(), runID)
		writeErr(w, http.StatusInternalServerError, "spawn: "+res.Error)
		return
	}

	// Update row with PID + log + worktree entry
	_, _ = a.db.Exec(`UPDATE runs SET agent_pid = ?, agent_log_path = ?, status = 'running'
		WHERE id = ?`, res.PID, res.LogPath, runID)
	wtID := "wt_" + uuid.New().String()[:12]
	_, _ = a.db.Exec(`INSERT INTO worktrees (id, run_id, path, branch, created_at)
		VALUES (?, ?, ?, ?, ?)`, wtID, runID, worktreePath, branch, now)

	a.sse.Publish(runID, sseEvent{Event: "status", Data: `{"status":"running","pid":` + strconv.Itoa(res.PID) + `}`})

	writeJSON(w, http.StatusCreated, createRunResponse{
		RunID:        runID,
		Branch:       branch,
		WorktreePath: worktreePath,
		SSEURL:       fmt.Sprintf("/api/runs/%s/sse", runID),
	})
}

// GET /api/runs?status=&app=&since=
func (a *App) handleListRuns(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	where := []string{}
	args := []interface{}{}
	if s := q.Get("status"); s != "" {
		where = append(where, "status = ?")
		args = append(args, s)
	}
	if app := q.Get("app"); app != "" {
		where = append(where, "kanban_app = ?")
		args = append(args, app)
	}
	if since := q.Get("since"); since != "" {
		if ts, err := strconv.ParseInt(since, 10, 64); err == nil {
			where = append(where, "started_at >= ?")
			args = append(args, ts)
		}
	}
	sqlStr := `SELECT id, workflow_id, issue_id, card_id, kanban_app, mode, branch, worktree_path,
		status, started_at, finished_at, agent_pid, agent_log_path, error
		FROM runs`
	if len(where) > 0 {
		sqlStr += " WHERE " + strings.Join(where, " AND ")
	}
	sqlStr += " ORDER BY started_at DESC LIMIT 200"
	rows, err := a.db.Query(sqlStr, args...)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "query: "+err.Error())
		return
	}
	defer rows.Close()
	out := []Run{}
	for rows.Next() {
		run, err := scanRun(rows)
		if err != nil {
			writeErr(w, http.StatusInternalServerError, "scan: "+err.Error())
			return
		}
		out = append(out, run)
	}
	writeJSON(w, http.StatusOK, out)
}

// GET /api/runs/:id
func (a *App) handleGetRun(w http.ResponseWriter, r *http.Request, id string) {
	row := a.db.QueryRow(`SELECT id, workflow_id, issue_id, card_id, kanban_app, mode, branch, worktree_path,
		status, started_at, finished_at, agent_pid, agent_log_path, error
		FROM runs WHERE id = ?`, id)
	run, err := scanRun(row)
	if errors.Is(err, sql.ErrNoRows) {
		writeErr(w, http.StatusNotFound, "run not found")
		return
	}
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "scan: "+err.Error())
		return
	}
	// Include dod items
	items, _ := listDodItems(a.db, id)
	writeJSON(w, http.StatusOK, map[string]interface{}{
		"run":       run,
		"dod_items": items,
	})
}

// POST /api/runs/:id/evidence
type evidenceRequest struct {
	ItemID      string  `json:"item_id"`
	ItemKey     string  `json:"item_key"`
	Kind        string  `json:"kind"`
	PayloadPath *string `json:"payload_path,omitempty"`
	PayloadURL  *string `json:"payload_url,omitempty"`
	PayloadText *string `json:"payload_text,omitempty"`
}

func (a *App) handleAttachEvidence(w http.ResponseWriter, r *http.Request, runID string) {
	var req evidenceRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "invalid json: "+err.Error())
		return
	}
	itemID := req.ItemID
	if itemID == "" && req.ItemKey != "" {
		// auto-create item if key provided
		it, err := createDodItem(a.db, runID, req.ItemKey, "manual", "", true)
		if err != nil {
			writeErr(w, http.StatusInternalServerError, "auto-create item: "+err.Error())
			return
		}
		itemID = it.ID
	}
	if itemID == "" {
		writeErr(w, http.StatusBadRequest, "item_id or item_key required")
		return
	}
	if req.Kind == "" {
		req.Kind = "text"
	}
	ev, err := attachEvidence(a.db, itemID, req.Kind, req.PayloadPath, req.PayloadURL, req.PayloadText)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "attach: "+err.Error())
		return
	}
	a.sse.Publish(runID, sseEvent{Event: "evidence", Data: `{"item_id":"` + itemID + `","evidence_id":"` + ev.ID + `"}`})
	writeJSON(w, http.StatusCreated, ev)
}

// POST /api/runs/:id/evidence/:eid/validate
type validateRequest struct {
	ValidatedBy string `json:"validated_by"`
}

func (a *App) handleValidateEvidence(w http.ResponseWriter, r *http.Request, runID, evID string) {
	var req validateRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "invalid json: "+err.Error())
		return
	}
	if req.ValidatedBy == "" {
		req.ValidatedBy = "human"
	}
	if err := validateEvidence(a.db, evID, req.ValidatedBy); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeErr(w, http.StatusNotFound, "evidence not found")
			return
		}
		writeErr(w, http.StatusInternalServerError, "validate: "+err.Error())
		return
	}
	a.sse.Publish(runID, sseEvent{Event: "validated", Data: `{"evidence_id":"` + evID + `","validated_by":"` + req.ValidatedBy + `"}`})
	writeJSON(w, http.StatusOK, map[string]string{"status": "validated"})
}

// POST /api/runs/:id/merge
func (a *App) handleMergeRun(w http.ResponseWriter, r *http.Request, runID string) {
	open, err := dodGateOpen(a.db, runID)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "gate check: "+err.Error())
		return
	}
	if !open {
		writeErr(w, http.StatusPreconditionFailed, "dod gate closed — required items not validated")
		return
	}
	row := a.db.QueryRow(`SELECT branch FROM runs WHERE id = ?`, runID)
	var branch string
	if err := row.Scan(&branch); err != nil {
		writeErr(w, http.StatusNotFound, "run not found")
		return
	}
	out, err := exec.Command("git", "-C", a.cfg.RepoRoot, "merge", "--no-ff", branch).CombinedOutput()
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "merge: "+err.Error()+": "+string(out))
		return
	}
	_, _ = a.db.Exec(`UPDATE runs SET status = 'merged', finished_at = ? WHERE id = ?`,
		time.Now().Unix(), runID)
	a.sse.Publish(runID, sseEvent{Event: "merged", Data: `{"branch":"` + branch + `"}`})
	writeJSON(w, http.StatusOK, map[string]string{"status": "merged", "branch": branch})
}

// POST /api/runs/:id/abort
func (a *App) handleAbortRun(w http.ResponseWriter, r *http.Request, runID string) {
	row := a.db.QueryRow(`SELECT agent_pid, worktree_path, branch FROM runs WHERE id = ?`, runID)
	var pidNS sql.NullInt64
	var wt, branch string
	if err := row.Scan(&pidNS, &wt, &branch); err != nil {
		writeErr(w, http.StatusNotFound, "run not found")
		return
	}
	pid := 0
	if pidNS.Valid {
		pid = int(pidNS.Int64)
	}
	if err := Cleanup(a.cfg.RepoRoot, pid, wt, branch); err != nil {
		// non-fatal; record but continue
		fmt.Println("cleanup warning:", err)
	}
	now := time.Now().Unix()
	_, _ = a.db.Exec(`UPDATE runs SET status = 'aborted', finished_at = ? WHERE id = ?`, now, runID)
	_, _ = a.db.Exec(`UPDATE worktrees SET removed_at = ? WHERE run_id = ? AND removed_at IS NULL`, now, runID)
	a.sse.Publish(runID, sseEvent{Event: "aborted", Data: `{"run_id":"` + runID + `"}`})
	writeJSON(w, http.StatusOK, map[string]string{"status": "aborted"})
}

// --- helpers ---

type rowScanner interface {
	Scan(dest ...interface{}) error
}

func scanRun(s rowScanner) (Run, error) {
	var r Run
	var workflowID, issueID, cardID, kanbanApp, logPath, errStr sql.NullString
	var finishedAt sql.NullInt64
	var agentPID sql.NullInt64
	err := s.Scan(&r.ID, &workflowID, &issueID, &cardID, &kanbanApp, &r.Mode, &r.Branch, &r.WorktreePath,
		&r.Status, &r.StartedAt, &finishedAt, &agentPID, &logPath, &errStr)
	if err != nil {
		return r, err
	}
	if workflowID.Valid {
		s := workflowID.String
		r.WorkflowID = &s
	}
	if issueID.Valid {
		s := issueID.String
		r.IssueID = &s
	}
	if cardID.Valid {
		s := cardID.String
		r.CardID = &s
	}
	if kanbanApp.Valid {
		s := kanbanApp.String
		r.KanbanApp = &s
	}
	if finishedAt.Valid {
		v := finishedAt.Int64
		r.FinishedAt = &v
	}
	if agentPID.Valid {
		v := int(agentPID.Int64)
		r.AgentPID = &v
	}
	if logPath.Valid {
		s := logPath.String
		r.AgentLogPath = &s
	}
	if errStr.Valid {
		s := errStr.String
		r.Error = &s
	}
	return r, nil
}

func nullStrFromS(s string) interface{} {
	if s == "" {
		return nil
	}
	return s
}
