package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"net/url"
	"sort"
	"strings"

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

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

type APIError struct {
	StatusCode int
	Code       interface{}
	Message    string
}

type MultipartFile struct {
	FieldName   string
	FileName    string
	ContentType string
	Reader      io.Reader
}

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) {
	var bodyReader io.Reader
	contentType := ""
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(data)
		contentType = "application/json"
	}

	return c.doRequest(method, path, bodyReader, contentType, query)
}

func (c *Client) PostMultipart(path string, fields map[string]string, files []MultipartFile) (*output.Envelope, error) {
	if len(files) == 0 {
		return nil, fmt.Errorf("at least one multipart file is required")
	}

	reader, writer := io.Pipe()
	form := multipart.NewWriter(writer)

	go func() {
		writeErr := writeMultipartBody(form, fields, files)
		closeErr := form.Close()
		if writeErr == nil {
			writeErr = closeErr
		}
		if writeErr != nil {
			_ = writer.CloseWithError(writeErr)
			return
		}
		_ = writer.Close()
	}()

	return c.doRequest(http.MethodPost, path, reader, form.FormDataContentType(), nil)
}

func (c *Client) doRequest(method, path string, body io.Reader, contentType string, query url.Values) (*output.Envelope, error) {
	fullURL := c.apiURL(path, query)

	req, err := http.NewRequest(method, fullURL, body)
	if err != nil {
		return nil, err
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}

	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 {
		previewLen := len(respData)
		if previewLen > 200 {
			previewLen = 200
		}
		fmt.Printf("<- %d %s\n", resp.StatusCode, string(respData[:previewLen]))
	}

	return parseResponseEnvelope(resp.StatusCode, respData)
}

func writeMultipartBody(form *multipart.Writer, fields map[string]string, files []MultipartFile) error {
	fieldNames := make([]string, 0, len(fields))
	for name := range fields {
		fieldNames = append(fieldNames, name)
	}
	sort.Strings(fieldNames)

	for _, name := range fieldNames {
		if err := form.WriteField(name, fields[name]); err != nil {
			return err
		}
	}

	for _, file := range files {
		if file.Reader == nil {
			return fmt.Errorf("multipart file reader is required")
		}
		part, err := createMultipartPart(form, file)
		if err != nil {
			return err
		}
		if _, err := io.Copy(part, file.Reader); err != nil {
			return err
		}
	}

	return nil
}

func createMultipartPart(form *multipart.Writer, file MultipartFile) (io.Writer, error) {
	fieldName := strings.TrimSpace(file.FieldName)
	if fieldName == "" {
		fieldName = "file"
	}
	fileName := strings.TrimSpace(file.FileName)
	if fileName == "" {
		fileName = fieldName
	}
	if strings.TrimSpace(file.ContentType) == "" {
		return form.CreateFormFile(fieldName, fileName)
	}

	header := make(textproto.MIMEHeader)
	header.Set("Content-Disposition", fmt.Sprintf(`form-data; name=%q; filename=%q`, fieldName, fileName))
	header.Set("Content-Type", file.ContentType)
	return form.CreatePart(header)
}

func (c *Client) apiURL(path string, query url.Values) string {
	path = normalizeAPIPath(c.BaseURL, path)

	// Append .json suffix if not already present (GitLink API convention)
	// Handle paths that may already contain query strings (e.g., /path?key=val)
	if idx := strings.Index(path, "?"); idx != -1 {
		basePath := path[:idx]
		queryStr := path[idx:]
		if shouldAppendJSONSuffix(basePath) {
			path = basePath + ".json" + queryStr
		}
	} else if shouldAppendJSONSuffix(path) {
		path += ".json"
	}

	fullURL := c.BaseURL + path
	if len(query) > 0 {
		sep := "?"
		if strings.Contains(fullURL, "?") {
			sep = "&"
		}
		fullURL += sep + query.Encode()
	}
	return fullURL
}

func parseResponseEnvelope(statusCode int, respData []byte) (*output.Envelope, error) {
	// Check HTTP-level errors
	if statusCode >= 400 {
		return nil, &APIError{
			StatusCode: statusCode,
			Code:       statusCode,
			Message:    fmt.Sprintf("HTTP %d: %s", statusCode, strings.TrimSpace(string(respData))),
		}
	}

	// 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)
			suggestion := suggestFix(int(statusCode))
			return output.ErrorEnvelope(int(statusCode), msg, suggestion), &APIError{
				StatusCode: int(statusCode),
				Code:       int(statusCode),
				Message:    msg,
			}
		}
	}

	// 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 shouldAppendJSONSuffix(path string) bool {
	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
}

func normalizeAPIPath(baseURL, path string) string {
	if strings.HasSuffix(strings.TrimRight(baseURL, "/"), "/api") {
		switch {
		case path == "/api":
			return ""
		case strings.HasPrefix(path, "/api/"):
			return strings.TrimPrefix(path, "/api")
		}
	}
	return path
}

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)
}

func suggestFix(code int) string {
	switch code {
	case 401:
		return "请先运行 gitlink-cli auth login 登录"
	case 403:
		return "权限不足，请确认账户权限或联系项目管理员"
	case 404:
		return "资源不存在，请检查 owner/repo/id 是否正确"
	case 422:
		return "参数校验失败，请检查请求参数"
	default:
		return ""
	}
}
