package api

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type Client struct {
	BaseURL    string
	Token      string
	HTTPClient *http.Client
}

func New(baseURL, token string) *Client {
	return &Client{
		BaseURL: strings.TrimRight(baseURL, "/"),
		Token:   token,
		HTTPClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (c *Client) do(method, path string, body interface{}) (*http.Response, error) {
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("marshal request body: %w", err)
		}
		bodyReader = bytes.NewReader(data)
	}
	u := c.BaseURL + path
	req, err := http.NewRequest(method, u, bodyReader)
	if err != nil {
		return nil, err
	}
	if c.Token != "" {
		req.Header.Set("Authorization", "Bearer "+c.Token)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	return c.HTTPClient.Do(req)
}

func (c *Client) doJSON(method, path string, body interface{}, result interface{}) error {
	resp, err := c.do(method, path, body)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		raw, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("API %s %s returned %d: %s", method, path, resp.StatusCode, string(raw))
	}
	if result != nil {
		return json.NewDecoder(resp.Body).Decode(result)
	}
	return nil
}

// --- Auth ---

type LoginRequest struct {
	Email string `json:"email"`
}
type ApikeyLoginRequest struct {
	APIKey string `json:"api_key"`
}
type loginResponseRaw struct {
	Token string `json:"token"`
	User  struct {
		ID    string `json:"id"`
		Email string `json:"email"`
	} `json:"user"`
}
type LoginResponse struct {
	Token string
	Email string
}
type MeResponse struct {
	ID    string `json:"id"`
	Email string `json:"email"`
}

func (c *Client) Login(email string) (*LoginResponse, error) {
	var raw loginResponseRaw
	err := c.doJSON("POST", "/api/auth/login", LoginRequest{Email: email}, &raw)
	if err != nil {
		return nil, err
	}
	return &LoginResponse{Token: raw.Token, Email: raw.User.Email}, nil
}

// LoginWithAPIKey exchanges a long-lived API key for a short-lived session token.
func (c *Client) LoginWithAPIKey(apiKey string) (*LoginResponse, error) {
	var raw loginResponseRaw
	err := c.doJSON("POST", "/api/auth/apikey", ApikeyLoginRequest{APIKey: apiKey}, &raw)
	if err != nil {
		return nil, err
	}
	return &LoginResponse{Token: raw.Token, Email: raw.User.Email}, nil
}

func (c *Client) Me() (*MeResponse, error) {
	var res MeResponse
	err := c.doJSON("GET", "/api/auth/me", nil, &res)
	return &res, err
}

// --- Workspaces ---

// FlexBool handles JSON fields that may come as bool (false/true) or number (0/1).
type FlexBool bool

func (f *FlexBool) UnmarshalJSON(data []byte) error {
	s := strings.TrimSpace(string(data))
	switch s {
	case "true", "1":
		*f = true
	default:
		*f = false
	}
	return nil
}

type Workspace struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	UserID      string   `json:"user_id,omitempty"`
	IsShared    FlexBool `json:"is_shared,omitempty"`
	IsExample   FlexBool `json:"is_example,omitempty"`
	CreatedAt   string   `json:"created_at,omitempty"`
	UpdatedAt   string   `json:"updated_at,omitempty"`
}

func (w *Workspace) Shared() bool {
	return bool(w.IsShared)
}

type CreateWorkspaceRequest struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
}

func (c *Client) ListWorkspaces() ([]Workspace, error) {
	var wrapper struct {
		Workspaces []Workspace `json:"workspaces"`
	}
	err := c.doJSON("GET", "/api/workspaces", nil, &wrapper)
	return wrapper.Workspaces, err
}

func (c *Client) CreateWorkspace(name, description string) (*Workspace, error) {
	var res Workspace
	err := c.doJSON("POST", "/api/workspaces", CreateWorkspaceRequest{
		Name:        name,
		Description: description,
	}, &res)
	if err != nil {
		return nil, err
	}
	_ = c.doJSON("POST", fmt.Sprintf("/api/workspace/init?workspace_id=%s", url.QueryEscape(res.ID)), nil, nil)
	return &res, nil
}

func (c *Client) GetWorkspace(id string) (*Workspace, error) {
	var res Workspace
	err := c.doJSON("GET", fmt.Sprintf("/api/workspaces/%s", url.PathEscape(id)), nil, &res)
	return &res, err
}

func (c *Client) UpdateWorkspace(id, name, description string) error {
	body := map[string]string{}
	if name != "" {
		body["name"] = name
	}
	if description != "" {
		body["description"] = description
	}
	return c.doJSON("PUT", fmt.Sprintf("/api/workspaces/%s", url.PathEscape(id)), body, nil)
}

func (c *Client) DeleteWorkspace(id string) error {
	return c.doJSON("DELETE", fmt.Sprintf("/api/workspaces/%s", url.PathEscape(id)), nil, nil)
}

// --- Sources ---

type Source struct {
	ID            string `json:"id"`
	Type          string `json:"type"`
	Name          string `json:"name"`
	Description   string `json:"description,omitempty"`
	IssueID       string `json:"issueId,omitempty"`
	IssueDir      string `json:"issueDir,omitempty"`
	Status        string `json:"status,omitempty"`
	StatusMessage string `json:"statusMessage,omitempty"`
	TaskID        string `json:"taskId,omitempty"`
	ReadOnly      bool   `json:"readOnly,omitempty"`
	CreatedAt     string `json:"createdAt,omitempty"`
}

func (c *Client) GetSources(workspaceID string) ([]Source, error) {
	var wrapper struct {
		Sources []Source `json:"sources"`
	}
	err := c.doJSON("GET", fmt.Sprintf("/api/workspace/%s/sources", url.PathEscape(workspaceID)), nil, &wrapper)
	return wrapper.Sources, err
}

func (c *Client) SaveSources(workspaceID string, sources []Source) error {
	body := map[string]interface{}{"sources": sources}
	return c.doJSON("PUT", fmt.Sprintf("/api/workspace/%s/sources", url.PathEscape(workspaceID)), body, nil)
}

// AddSource sends a source (without ID) to the backend, which generates and returns the ID.
func (c *Client) AddSource(workspaceID string, source Source) (*Source, error) {
	var result Source
	err := c.doJSON("POST", fmt.Sprintf("/api/workspace/%s/sources", url.PathEscape(workspaceID)), source, &result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// DeleteSource removes a single source by ID.
func (c *Client) DeleteSource(workspaceID, sourceID string) error {
	return c.doJSON("DELETE", fmt.Sprintf("/api/workspace/%s/sources/%s", url.PathEscape(workspaceID), url.PathEscape(sourceID)), nil, nil)
}

type PrepareRequest struct {
	IssueID     string `json:"issue_id"`
	WorkspaceID string `json:"workspace_id"`
}
type PrepareResponse struct {
	TaskID string `json:"task_id"`
	Status string `json:"status"`
}
type PrepareStatusResponse struct {
	TaskID   string `json:"task_id"`
	Status   string `json:"status"`
	IssueDir string `json:"issue_dir,omitempty"`
	Message  string `json:"message,omitempty"`
	Error    string `json:"error,omitempty"`
}

func (c *Client) PrepareSource(issueID, workspaceID string) (*PrepareResponse, error) {
	var res PrepareResponse
	err := c.doJSON("POST", "/api/sources/prepare", PrepareRequest{
		IssueID:     issueID,
		WorkspaceID: workspaceID,
	}, &res)
	return &res, err
}

func (c *Client) GetPrepareStatus(taskID string) (*PrepareStatusResponse, error) {
	var res PrepareStatusResponse
	err := c.doJSON("GET", fmt.Sprintf("/api/sources/prepare/%s", url.PathEscape(taskID)), nil, &res)
	return &res, err
}

// --- Artifacts ---

type Artifact struct {
	ID       string          `json:"id"`
	Type     string          `json:"type"`
	Title    string          `json:"title"`
	SourceID string          `json:"sourceId,omitempty"`
	Data     json.RawMessage `json:"data,omitempty"`
}

func (c *Client) GetArtifacts(workspaceID string) ([]Artifact, error) {
	var wrapper struct {
		Artifacts []Artifact `json:"artifacts"`
	}
	err := c.doJSON("GET", fmt.Sprintf("/api/workspace/%s/artifacts", url.PathEscape(workspaceID)), nil, &wrapper)
	return wrapper.Artifacts, err
}

// --- Messages ---

// Message mirrors the UI Message type so CLI-originated messages are
// rendered correctly when the owner or viewers open the workspace in the UI.
type Message struct {
	ID        string          `json:"id"`
	Role      string          `json:"role"`
	Content   string          `json:"content"`
	Timestamp string          `json:"timestamp"`
	Parts     json.RawMessage `json:"parts,omitempty"`
}

func (c *Client) AppendMessages(workspaceID string, messages []Message) error {
	body := map[string]interface{}{"messages": messages}
	return c.doJSON("POST", fmt.Sprintf("/api/workspace/%s/messages", url.PathEscape(workspaceID)), body, nil)
}

func (c *Client) ClearMessages(workspaceID string) error {
	return c.doJSON("DELETE", fmt.Sprintf("/api/workspace/%s/messages", url.PathEscape(workspaceID)), nil, nil)
}

// --- Files ---

type FileEntry struct {
	Name     string      `json:"name"`
	Path     string      `json:"path"`
	Type     string      `json:"type"`
	Size     string      `json:"size,omitempty"`
	Children []FileEntry `json:"children,omitempty"`
}

func (f *FileEntry) IsDir() bool {
	return f.Type == "directory"
}

func (c *Client) ListFiles(workspaceID string) ([]FileEntry, error) {
	var wrapper struct {
		Files []FileEntry `json:"files"`
	}
	err := c.doJSON("GET", fmt.Sprintf("/api/workspace/%s/files", url.PathEscape(workspaceID)), nil, &wrapper)
	return wrapper.Files, err
}

func (c *Client) GetFileContent(workspaceID, path string) (string, error) {
	resp, err := c.do("GET", fmt.Sprintf("/api/workspace/%s/file?path=%s", url.PathEscape(workspaceID), url.QueryEscape(path)), nil)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		raw, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("API returned %d: %s", resp.StatusCode, string(raw))
	}
	data, err := io.ReadAll(resp.Body)
	return string(data), err
}

// --- Debug ---

type ContextDebugReport struct {
	WorkspaceID   string `json:"workspace_id"`
	WorkspaceDir  string `json:"workspace_dir"`
	GeneratedAt   string `json:"generated_at"`
	ActualContext struct {
		ContextUsed      int      `json:"context_used"`
		ContextSize      int      `json:"context_size"`
		Percent          *float64 `json:"percent"`
		SessionID        string   `json:"session_id"`
		CreatedAt        string   `json:"created_at"`
		InputTokens      int      `json:"input_tokens"`
		OutputTokens     int      `json:"output_tokens"`
		CacheReadTokens  int      `json:"cache_read_tokens"`
		CacheWriteTokens int      `json:"cache_write_tokens"`
	} `json:"actual_context"`
	EstimateModel struct {
		TokenEstimator          string `json:"token_estimator"`
		IncludedEstimatedTokens int    `json:"included_estimated_tokens"`
		ScaledToActualContext   bool   `json:"scaled_to_actual_context"`
	} `json:"estimate_model"`
	Categories []ContextDebugCategory `json:"categories"`
	Notes      []string               `json:"notes"`
}

type ContextDebugCategory struct {
	Key                       string                `json:"key"`
	Label                     string                `json:"label"`
	Scope                     string                `json:"scope"`
	IncludedInContextEstimate bool                  `json:"included_in_context_estimate"`
	Description               string                `json:"description"`
	Bytes                     int                   `json:"bytes"`
	EstimatedTokens           int                   `json:"estimated_tokens"`
	ItemCount                 int                   `json:"item_count"`
	ScaledContextTokens       *int                  `json:"scaled_context_tokens"`
	PercentOfUsedContext      *float64              `json:"percent_of_used_context"`
	PercentOfContextWindow    *float64              `json:"percent_of_context_window"`
	Examples                  []ContextDebugExample `json:"examples"`
}

type ContextDebugExample struct {
	Path                   string   `json:"path"`
	Label                  string   `json:"label"`
	Detail                 string   `json:"detail"`
	SourceRef              string   `json:"source_ref"`
	Kind                   string   `json:"kind"`
	Bytes                  int      `json:"bytes"`
	EstimatedTokens        int      `json:"estimated_tokens"`
	ScaledContextTokens    *int     `json:"scaled_context_tokens"`
	PercentOfUsedContext   *float64 `json:"percent_of_used_context"`
	PercentOfContextWindow *float64 `json:"percent_of_context_window"`
}

func (c *Client) GetContextDebug(workspaceID string) (*ContextDebugReport, error) {
	var res ContextDebugReport
	err := c.doJSON("GET", fmt.Sprintf("/api/workspace/%s/debug/context", url.PathEscape(workspaceID)), nil, &res)
	return &res, err
}

// --- Chat SSE ---

type SSEEvent struct {
	ID   string
	Type string
	Data json.RawMessage
}
type SSETextData struct {
	Text string `json:"text"`
}
type SSEToolData struct {
	ID     string `json:"id"`
	Title  string `json:"title"`
	Status string `json:"status"`
	Input  string `json:"input"`
	Output string `json:"output"`
}
type SSEErrorData struct {
	Error string `json:"error"`
}

type SSECompleteData struct {
	Status string `json:"status"`
}

type AgentRun struct {
	ID                 string `json:"id"`
	SessionID          string `json:"session_id"`
	WorkspaceID        string `json:"workspace_id"`
	UserMessageID      string `json:"user_message_id"`
	AssistantMessageID string `json:"assistant_message_id"`
	Status             string `json:"status"`
	Error              string `json:"error"`
}

type AgentSession struct {
	ID          string `json:"id"`
	WorkspaceID string `json:"workspace_id"`
}

type CreateAgentTurnRequest struct {
	Input         string   `json:"input"`
	ClientTurnID  string   `json:"client_turn_id"`
	AttachmentIDs []string `json:"attachment_ids"`
}

func (c *Client) CreateAgentRun(message, workspaceID, userMessageID string) (*AgentRun, error) {
	var session AgentSession
	if err := c.doJSON("POST", "/api/sessions", map[string]string{
		"workspace_id": workspaceID,
	}, &session); err != nil {
		return nil, err
	}
	var turn AgentRun
	err := c.doJSON("POST", fmt.Sprintf(
		"/api/sessions/%s/turns?wait=false",
		url.PathEscape(session.ID),
	), CreateAgentTurnRequest{
		Input:         message,
		ClientTurnID:  userMessageID,
		AttachmentIDs: []string{},
	}, &turn)
	return &turn, err
}

func (c *Client) CancelAgentRun(sessionID string) (*AgentRun, error) {
	var turn AgentRun
	err := c.doJSON(
		"POST",
		fmt.Sprintf("/api/sessions/%s/turns/stop", url.PathEscape(sessionID)),
		nil,
		&turn,
	)
	return &turn, err
}

type transcriptRecord struct {
	TurnID string          `json:"turn_id"`
	Data   json.RawMessage `json:"data"`
}

func (c *Client) AgentRunEvents(ctx context.Context, run *AgentRun) (<-chan SSEEvent, error) {
	streamCtx, cancel := context.WithCancel(ctx)
	rawEvents, err := c.agentTranscriptEvents(streamCtx, run.SessionID)
	if err != nil {
		cancel()
		return nil, err
	}
	out := make(chan SSEEvent, 64)
	go func() {
		defer cancel()
		defer close(out)
		for raw := range rawEvents {
			var records []transcriptRecord
			switch raw.Type {
			case "record":
				var envelope struct {
					Record transcriptRecord `json:"record"`
				}
				if json.Unmarshal(raw.Data, &envelope) == nil {
					records = append(records, envelope.Record)
				}
			case "sync", "reset":
				var envelope struct {
					Transcript struct {
						Records []transcriptRecord `json:"records"`
					} `json:"transcript"`
				}
				if json.Unmarshal(raw.Data, &envelope) == nil {
					records = envelope.Transcript.Records
				}
			}
			for _, record := range records {
				if record.TurnID != run.ID {
					continue
				}
				var payload struct {
					Type string `json:"type"`
				}
				if json.Unmarshal(record.Data, &payload) != nil || payload.Type == "" {
					continue
				}
				select {
				case out <- SSEEvent{ID: raw.ID, Type: payload.Type, Data: record.Data}:
				case <-streamCtx.Done():
					return
				}
				if payload.Type == "complete" {
					return
				}
			}
		}
	}()
	return out, nil
}

func (c *Client) agentTranscriptEvents(ctx context.Context, sessionID string) (<-chan SSEEvent, error) {
	resp, err := c.openAgentTranscriptEvents(ctx, sessionID, "")
	if err != nil {
		return nil, err
	}

	ch := make(chan SSEEvent, 64)
	go func() {
		defer close(ch)
		lastEventID := ""
		for {
			observedID, completed, _ := parseSSEContext(ctx, resp.Body, ch)
			resp.Body.Close()
			if observedID != "" {
				lastEventID = observedID
			}
			if completed || ctx.Err() != nil {
				return
			}

			timer := time.NewTimer(500 * time.Millisecond)
			select {
			case <-ctx.Done():
				timer.Stop()
				return
			case <-timer.C:
			}
			for {
				resp, err = c.openAgentTranscriptEvents(ctx, sessionID, lastEventID)
				if err == nil {
					break
				}
				timer.Reset(time.Second)
				select {
				case <-ctx.Done():
					timer.Stop()
					return
				case <-timer.C:
				}
			}
		}
	}()
	return ch, nil
}

func (c *Client) openAgentTranscriptEvents(ctx context.Context, sessionID, lastEventID string) (*http.Response, error) {
	u := fmt.Sprintf(
		"%s/api/sessions/%s/transcript/events",
		c.BaseURL,
		url.PathEscape(sessionID),
	)
	req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)
	req.Header.Set("Accept", "text/event-stream")
	req.Header.Set("Cache-Control", "no-cache")
	if lastEventID != "" {
		req.Header.Set("Last-Event-ID", lastEventID)
	}

	streamClient := *c.HTTPClient
	streamClient.Timeout = 0
	resp, err := streamClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("connect to agent run events: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		raw, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return nil, fmt.Errorf("agent run events returned %d: %s", resp.StatusCode, string(raw))
	}

	return resp, nil
}

func (c *Client) ChatStream(message, workspaceID string) (<-chan SSEEvent, error) {
	u := fmt.Sprintf("%s/api/chat/stream?message=%s&workspace_id=%s&token=%s",
		c.BaseURL,
		url.QueryEscape(message),
		url.QueryEscape(workspaceID),
		url.QueryEscape(c.Token),
	)
	httpClient := &http.Client{Timeout: 0}
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "text/event-stream")
	req.Header.Set("Cache-Control", "no-cache")

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("connect to chat stream: %w", err)
	}
	if resp.StatusCode != 200 {
		raw, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return nil, fmt.Errorf("chat stream returned %d: %s", resp.StatusCode, string(raw))
	}

	ch := make(chan SSEEvent, 64)
	go func() {
		defer resp.Body.Close()
		defer close(ch)
		parseSSE(resp.Body, ch)
	}()
	return ch, nil
}

func parseSSE(r io.Reader, ch chan<- SSEEvent) {
	_, _, _ = parseSSEContext(context.Background(), r, ch)
}

func parseSSEContext(ctx context.Context, r io.Reader, ch chan<- SSEEvent) (string, bool, error) {
	scanner := bufio.NewScanner(r)
	scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
	var currentID string
	var currentEvent string
	var dataLines []string
	var lastEventID string

	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			if len(dataLines) > 0 {
				data := strings.Join(dataLines, "\n")
				eventType := currentEvent
				if eventType == "" {
					eventType = "message"
				}
				var raw map[string]json.RawMessage
				if err := json.Unmarshal([]byte(data), &raw); err == nil {
					if t, ok := raw["type"]; ok {
						var typ string
						if json.Unmarshal(t, &typ) == nil {
							eventType = typ
						}
					}
				}
				select {
				case ch <- SSEEvent{ID: currentID, Type: eventType, Data: json.RawMessage(data)}:
					if currentID != "" {
						lastEventID = currentID
					}
					if eventType == "complete" {
						return lastEventID, true, nil
					}
				case <-ctx.Done():
					return lastEventID, false, ctx.Err()
				}
			}
			currentID = ""
			currentEvent = ""
			dataLines = nil
			continue
		}
		if strings.HasPrefix(line, "id:") {
			currentID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
		} else if strings.HasPrefix(line, "event:") {
			currentEvent = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
		} else if strings.HasPrefix(line, "data:") {
			dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
		}
	}
	return lastEventID, false, scanner.Err()
}

// --- Workspace Export ---

// ExportTask mirrors the backend _export_tasks entry.
type ExportTask struct {
	TaskID        string `json:"task_id"`
	WorkspaceID   string `json:"workspace_id"`
	WorkspaceName string `json:"workspace_name"`
	Status        string `json:"status"`
	DownloadURL   string `json:"download_url"`
	S3Key         string `json:"s3_key"`
	Error         string `json:"error"`
	CreatedAt     string `json:"created_at"`
}

// StartExport kicks off a workspace export and returns the task ID.
func (c *Client) StartExport(workspaceID string) (*ExportTask, error) {
	var res ExportTask
	err := c.doJSON("POST", fmt.Sprintf("/api/workspace/%s/export", url.PathEscape(workspaceID)), nil, &res)
	return &res, err
}

// GetExportStatus polls the status of a single export task.
func (c *Client) GetExportStatus(workspaceID, taskID string) (*ExportTask, error) {
	var res ExportTask
	err := c.doJSON("GET", fmt.Sprintf("/api/workspace/%s/export/%s", url.PathEscape(workspaceID), url.PathEscape(taskID)), nil, &res)
	return &res, err
}

// ListExports returns all export tasks for the current user.
func (c *Client) ListExports() ([]ExportTask, error) {
	var wrapper struct {
		Exports []ExportTask `json:"exports"`
	}
	err := c.doJSON("GET", "/api/exports", nil, &wrapper)
	return wrapper.Exports, err
}
