package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/gitlink-org/gitlink-cli/internal/auth"
	"github.com/gitlink-org/gitlink-cli/internal/config"
	clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
	"github.com/gitlink-org/gitlink-cli/internal/output"
)

type Client struct {
	HTTP           *http.Client
	BaseURL        string
	Debug          bool
	SkipJSONSuffix bool
}

type APIError struct {
	StatusCode int
	Code       interface{}
	Message    string
	Kind       clierrors.ErrorKind
	Suggestion string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("[%v] %s", e.Code, e.Message)
}

func New() (*Client, error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, err
	}
	return &Client{
		HTTP:    auth.NewHTTPClient(),
		BaseURL: cfg.BaseURL,
	}, nil
}

func (c *Client) Do(method, path string, body interface{}, query url.Values) (*output.Envelope, error) {
	// Append .json suffix if not already present (GitLink API convention)
	// Handle paths that may already contain query strings (e.g., /path?key=val)
	if c.shouldAppendJSONSuffix(path) {
		if idx := strings.Index(path, "?"); idx != -1 {
			basePath := path[:idx]
			queryStr := path[idx:]
			path = basePath + ".json" + queryStr
		} else {
			path += ".json"
		}
	}
	fullURL := c.BaseURL + path
	if query != nil && len(query) > 0 {
		sep := "?"
		if strings.Contains(fullURL, "?") {
			sep = "&"
		}
		fullURL += sep + query.Encode()
	}

	// Replace path params
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(data)
	}

	req, err := http.NewRequest(method, fullURL, bodyReader)
	if err != nil {
		return nil, err
	}

	if c.Debug {
		fmt.Printf("→ %s %s\n", method, fullURL)
	}

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

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

	if c.Debug {
		fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)]))
	}

	// Check HTTP-level errors
	if resp.StatusCode >= 400 {
		info := lookupStatusInfo(resp.StatusCode)
		return nil, &APIError{
			StatusCode: resp.StatusCode,
			Code:       resp.StatusCode,
			Message:    fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
			Kind:       info.kind,
			Suggestion: info.suggestion,
		}
	}

	// Parse JSON
	var raw map[string]interface{}
	if err := json.Unmarshal(respData, &raw); err != nil {
		// Not JSON, return as-is
		return output.SuccessEnvelope(string(respData), nil), nil
	}

	// Check GitLink error-in-body pattern
	if status, ok := raw["status"]; ok {
		var statusCode float64
		switch v := status.(type) {
		case float64:
			statusCode = v
		case int:
			statusCode = float64(v)
		}
		if statusCode != 0 && statusCode != 200 && statusCode != 1 {
			msg, _ := raw["message"].(string)
			info := lookupStatusInfo(int(statusCode))
			return output.ErrorEnvelope(int(statusCode), msg, info.suggestion), &APIError{
				StatusCode: int(statusCode),
				Code:       int(statusCode),
				Message:    msg,
				Kind:       info.kind,
				Suggestion: info.suggestion,
			}
		}
	}

	// Auto-parse JSON string data (GitLink API quirk: some endpoints return data as JSON string)
	if dataStr, ok := raw["data"].(string); ok {
		var parsedData interface{}
		if err := json.Unmarshal([]byte(dataStr), &parsedData); err == nil {
			raw["data"] = json.RawMessage(dataStr)
		}
	}

	// Build meta from pagination info
	var meta *output.Meta
	if tc, ok := raw["total_count"]; ok {
		meta = &output.Meta{}
		if v, ok := tc.(float64); ok {
			meta.TotalCount = int(v)
		}
		if v, ok := raw["page"].(float64); ok {
			meta.Page = int(v)
		}
		if v, ok := raw["limit"].(float64); ok {
			meta.Limit = int(v)
		}
	}

	return output.SuccessEnvelope(raw, meta), nil
}

func (c *Client) Get(path string, query url.Values) (*output.Envelope, error) {
	return c.Do("GET", path, nil, query)
}

func (c *Client) Post(path string, body interface{}) (*output.Envelope, error) {
	return c.Do("POST", path, body, nil)
}

func (c *Client) Put(path string, body interface{}) (*output.Envelope, error) {
	return c.Do("PUT", path, body, nil)
}

func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error) {
	return c.Do("DELETE", path, nil, query)
}

type statusInfo struct {
	kind       clierrors.ErrorKind
	message    string
	suggestion string
}

var statusMessages = map[int]statusInfo{
	-2: {clierrors.KindAuth, "未登录或 Token 已过期",
		"运行 gitlink-cli auth login 重新登录，或检查 GITLINK_TOKEN 环境变量"},
	-1: {clierrors.KindInput, "参数校验失败",
		"检查必填参数是否缺失、参数格式是否正确，运行 gitlink-cli <命令> --help 查看用法"},
	0: {clierrors.KindUnknown, "操作失败", ""},
	// Standard HTTP codes
	401: {clierrors.KindAuth, "认证失败",
		"运行 gitlink-cli auth login 登录，或检查 GITLINK_TOKEN 环境变量"},
	403: {clierrors.KindForbidden, "权限不足",
		"请确认账号有此仓库的访问权限，或联系项目管理员"},
	404: {clierrors.KindNotFound, "资源不存在",
		"检查 owner/repo/id 是否正确，资源可能已被删除"},
	422: {clierrors.KindInput, "参数校验失败",
		"检查请求参数格式，运行 gitlink-cli <命令> --help 查看用法"},
	429: {clierrors.KindServer, "请求过于频繁",
		"稍等片刻后重试"},
	500: {clierrors.KindServer, "服务器内部错误",
		"稍等后重试，如持续出现请联系平台管理员"},
	502: {clierrors.KindServer, "网关错误",
		"服务器暂时不可用，稍等后重试"},
	503: {clierrors.KindServer, "服务暂时不可用",
		"服务器正在维护，稍等后重试"},
}

func lookupStatusInfo(code int) statusInfo {
	if info, ok := statusMessages[code]; ok {
		return info
	}
	return statusInfo{
		kind:    clierrors.KindUnknown,
		message: fmt.Sprintf("API 返回错误码 %d", code),
	}
}

// shouldAppendJSONSuffix reports whether the .json suffix should be appended to path.
// Returns false (skip append) when:
//   - c.SkipJSONSuffix is set (explicit opt-out for non-JSON endpoints such as gateway)
//   - path already ends with .json
//   - path matches the raw content pattern (e.g., /api/:owner/:repo/raw/...)
func (c *Client) shouldAppendJSONSuffix(path string) bool {
	if c.SkipJSONSuffix {
		return false
	}
	if strings.HasSuffix(path, ".json") {
		return false
	}
	parts := strings.Split(strings.Trim(path, "/"), "/")
	for i, part := range parts {
		if part == "raw" && i >= 2 && i+2 < len(parts) {
			return false
		}
	}
	return true
}
