package store

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/sdp/protocol"
)

// Deployment is a deployment row joined with the current state and
// container info. Used by the dashboard's history list and the
// "active deployment" badge in the sandbox view.
type Deployment struct {
	ID          string `json:"id"`
	SandboxID   string `json:"sandboxId,omitempty"`
	Repository  string `json:"repository"`
	Branch      string `json:"branch"`
	User        string `json:"user"`
	State       string `json:"state"`
	ContainerID string `json:"containerId,omitempty"`
	HostPort    int    `json:"hostPort,omitempty"`
	StartedAt   int64  `json:"startedAt"`
	CompletedAt int64  `json:"completedAt,omitempty"`
}

// StartDeployment records a new deployment row. Idempotent on id.
func (s *Store) StartDeployment(id, repo, branch, user string) error {
	_, err := s.db.Exec(
		`INSERT OR IGNORE INTO deployments(id, repository, branch, user, state, started_at) VALUES(?,?,?,?,?,?)`,
		id, repo, branch, user, "QUEUED", time.Now().UnixMilli(),
	)
	return err
}

// StartDeploymentInSandbox records a new deployment tied to a sandbox.
// sandboxID and hostPort are nullable; empty string / 0 stores NULL.
func (s *Store) StartDeploymentInSandbox(id, sandboxID, repo, branch, user string, hostPort int) error {
	var sb interface{}
	if sandboxID != "" {
		sb = sandboxID
	}
	var hp interface{}
	if hostPort > 0 {
		hp = hostPort
	}
	_, err := s.db.Exec(
		`INSERT OR IGNORE INTO deployments(id, sandbox_id, repository, branch, user, state, host_port, started_at)
		 VALUES(?,?,?,?,?,?,?,?)`,
		id, sb, repo, branch, user, "QUEUED", hp, time.Now().UnixMilli(),
	)
	return err
}

// FinishDeployment marks the final state. completed_at is set if state is terminal.
func (s *Store) FinishDeployment(id, state string) error {
	_, err := s.db.Exec(
		`UPDATE deployments SET state=?, completed_at=? WHERE id=?`,
		state, time.Now().UnixMilli(), id,
	)
	return err
}

// SetContainerID records the container id once a deploy is RUNNING.
func (s *Store) SetContainerID(id, containerID string) error {
	_, err := s.db.Exec(`UPDATE deployments SET container_id=? WHERE id=?`, containerID, id)
	return err
}

// MarkStage records a stage transition. ok=1 success, 0 failure.
func (s *Store) MarkStage(id, stage string, ok bool) error {
	v := 0
	if ok {
		v = 1
	}
	_, err := s.db.Exec(
		`INSERT INTO progress(deployment_id, stage, ok, at) VALUES(?,?,?,?)`,
		id, stage, v, time.Now().UnixMilli(),
	)
	return err
}

// AppendEvent writes an event. Log lines go to .log; progress/status hit SQLite.
// The deployment's running state is also updated so /api/deployments/{id} can
// serve a snapshot without replaying the whole log.
func (s *Store) AppendEvent(e protocol.Event) error {
	switch e.Kind {
	case "log":
		return s.appendLog(e)
	case "status":
		_, err := s.db.Exec(`UPDATE deployments SET state=? WHERE id=?`, e.State, e.DeploymentID)
		return err
	case "progress":
		return s.MarkStage(e.DeploymentID, e.Stage, e.State != "FAILED")
	}
	return nil
}

func (s *Store) appendLog(e protocol.Event) error {
	s.mu.Lock()
	f, ok := s.logs[e.DeploymentID]
	if !ok {
		path := filepath.Join(s.dir, "logs", e.DeploymentID+".log")
		var err error
		f, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
		if err != nil {
			s.mu.Unlock()
			return err
		}
		s.logs[e.DeploymentID] = f
	}
	s.mu.Unlock()
	ts := time.UnixMilli(e.At).Format("15:04:05.000")
	_, err := fmt.Fprintf(f, "%s %s\n", ts, e.Line)
	return err
}

// TailLogs returns the last n lines of a deployment's log file. Used by the
// dashboard on first connect to backfill.
func (s *Store) TailLogs(id string, n int) ([]string, error) {
	path := filepath.Join(s.dir, "logs", id+".log")
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	// ponytail: O(n) scan, fine for tail use. swap to a ring buffer if logs get huge.
	var lines []string
	start := 0
	for i, b := range data {
		if b == '\n' {
			lines = append(lines, string(data[start:i]))
			start = i + 1
		}
	}
	if start < len(data) {
		lines = append(lines, string(data[start:]))
	}
	if n > 0 && len(lines) > n {
		lines = lines[len(lines)-n:]
	}
	return lines, nil
}

// ListDeployments returns deployments, newest first. If sandboxID is set,
// filters to that sandbox. limit caps the result.
func (s *Store) ListDeployments(sandboxID string, limit int) ([]Deployment, error) {
	if limit <= 0 || limit > 500 {
		limit = 100
	}
	var (
		rows *sql.Rows
		err  error
	)
	if sandboxID == "" {
		rows, err = s.db.Query(
			`SELECT id, COALESCE(sandbox_id,''), repository, branch, COALESCE(user,''), state,
			        COALESCE(container_id,''), COALESCE(host_port,0), started_at, COALESCE(completed_at,0)
			 FROM deployments ORDER BY started_at DESC LIMIT ?`, limit)
	} else {
		rows, err = s.db.Query(
			`SELECT id, COALESCE(sandbox_id,''), repository, branch, COALESCE(user,''), state,
			        COALESCE(container_id,''), COALESCE(host_port,0), started_at, COALESCE(completed_at,0)
			 FROM deployments WHERE sandbox_id=? ORDER BY started_at DESC LIMIT ?`, sandboxID, limit)
	}
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Deployment
	for rows.Next() {
		var d Deployment
		if err := rows.Scan(&d.ID, &d.SandboxID, &d.Repository, &d.Branch, &d.User, &d.State,
			&d.ContainerID, &d.HostPort, &d.StartedAt, &d.CompletedAt); err != nil {
			return nil, err
		}
		out = append(out, d)
	}
	return out, rows.Err()
}

// LatestDeploymentBySandboxService returns the most recent deployment for
// the (sandbox, repo) pair, or nil if none.
func (s *Store) LatestDeploymentBySandboxService(sandboxID, repo string) (*Deployment, error) {
	row := s.db.QueryRow(
		`SELECT id, COALESCE(sandbox_id,''), repository, branch, COALESCE(user,''), state,
		        COALESCE(container_id,''), COALESCE(host_port,0), started_at, COALESCE(completed_at,0)
		 FROM deployments WHERE sandbox_id=? AND repository=? ORDER BY started_at DESC LIMIT 1`,
		sandboxID, repo)
	var d Deployment
	err := row.Scan(&d.ID, &d.SandboxID, &d.Repository, &d.Branch, &d.User, &d.State,
		&d.ContainerID, &d.HostPort, &d.StartedAt, &d.CompletedAt)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &d, nil
}

// GetDeployment returns one deployment by id, or nil if not found.
func (s *Store) GetDeployment(id string) (*Deployment, error) {
	row := s.db.QueryRow(
		`SELECT id, COALESCE(sandbox_id,''), repository, branch, COALESCE(user,''), state,
		        COALESCE(container_id,''), COALESCE(host_port,0), started_at, COALESCE(completed_at,0)
		 FROM deployments WHERE id=?`, id)
	var d Deployment
	err := row.Scan(&d.ID, &d.SandboxID, &d.Repository, &d.Branch, &d.User, &d.State,
		&d.ContainerID, &d.HostPort, &d.StartedAt, &d.CompletedAt)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &d, nil
}

// marshalJSON is a small helper for the json column on environments.
func marshalJSON(v map[string]string) (string, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

func unmarshalJSON(s string) (map[string]string, error) {
	if s == "" {
		return map[string]string{}, nil
	}
	var m map[string]string
	if err := json.Unmarshal([]byte(s), &m); err != nil {
		return nil, err
	}
	return m, nil
}
