package feichibridge

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

var ErrUnauthorized = errors.New("feichi API session unauthorized")

type APIClient struct {
	baseURL *url.URL
	headers http.Header
	client  *http.Client
	login   *loginSession
}

func NewAPIClient(baseURL string, headers map[string]string, timeout time.Duration) (*APIClient, error) {
	parsed, err := url.Parse(strings.TrimRight(strings.TrimSpace(baseURL), "/") + "/")
	if err != nil {
		return nil, fmt.Errorf("parse FEICHI_BASE_URL: %w", err)
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return nil, fmt.Errorf("FEICHI_BASE_URL must use http or https")
	}
	if timeout <= 0 {
		timeout = 15 * time.Second
	}
	out := &APIClient{
		baseURL: parsed,
		headers: make(http.Header),
		client:  &http.Client{Timeout: timeout},
	}
	for name, value := range headers {
		if strings.TrimSpace(name) != "" && strings.TrimSpace(value) != "" {
			out.headers.Set(name, value)
		}
	}
	return out, nil
}

func NewAuthenticatedAPIClient(baseURL string, credentials LoginCredentials, solver CaptchaSolver, timeout time.Duration) (*APIClient, error) {
	client, err := NewAPIClient(baseURL, nil, timeout)
	if err != nil {
		return nil, err
	}
	login, err := newLoginSession(credentials, solver)
	if err != nil {
		return nil, err
	}
	client.login = login
	return client, nil
}

func (c *APIClient) Vehicles(ctx context.Context) ([]Vehicle, error) {
	payload := map[string]any{
		"conditions": []map[string]string{
			{"name": "iccid", "value": ""},
			{"name": "powerMode", "value": ""},
		},
		"sort":  []map[string]string{{"name": "updateTime", "order": "desc"}},
		"start": 0,
		"limit": 100,
	}
	var envelope collectionEnvelope[Vehicle]
	if err := c.doJSON(ctx, http.MethodPost, "api/v1/sys/vehicleRealStatuss", payload, &envelope); err != nil {
		return nil, err
	}
	if !apiCodeOK(envelope.Code) {
		return nil, apiCodeError(envelope.Code)
	}
	return envelope.Data, nil
}

func (c *APIClient) Snapshot(ctx context.Context, vehicleID string) (Snapshot, error) {
	path := "api/v1/sys/vehicleRealStatussByVId/" + url.PathEscape(vehicleID) + "/-1"
	var envelope objectEnvelope[Snapshot]
	if err := c.doJSON(ctx, http.MethodGet, path, nil, &envelope); err != nil {
		return Snapshot{}, err
	}
	if !apiCodeOK(envelope.Code) {
		return Snapshot{}, apiCodeError(envelope.Code)
	}
	return envelope.Data, nil
}

func (c *APIClient) History(ctx context.Context, vin string, begin, end time.Time) ([]Record, error) {
	payload := map[string]any{
		"conditions": []map[string]string{
			{"name": "queryType", "value": "0"},
			{"name": "queryContent", "value": vin},
			{"name": "beginTime", "value": begin.In(shanghai).Format("2006-01-02 15:04:05")},
			{"name": "endTime", "value": end.In(shanghai).Format("2006-01-02 15:04:05")},
			{"name": "hisdataType", "value": "0"},
		},
		"start": 0,
		"limit": 100,
	}
	var envelope collectionEnvelope[historySegment]
	if err := c.doJSON(ctx, http.MethodPost, "api/v1/sys/hisdataQuerys", payload, &envelope); err != nil {
		return nil, err
	}
	if !apiCodeOK(envelope.Code) {
		return nil, apiCodeError(envelope.Code)
	}
	var records []Record
	for _, segment := range envelope.Data {
		records = append(records, segment.Subdata...)
	}
	return records, nil
}

func (c *APIClient) doJSON(ctx context.Context, method, path string, body any, output any) error {
	if c.login != nil {
		if err := c.login.ensure(ctx, c); err != nil {
			return err
		}
	}
	for attempt := 0; attempt < 2; attempt++ {
		_, err := c.rawJSON(ctx, method, path, body, output)
		if !errors.Is(err, ErrUnauthorized) || c.login == nil || attempt == 1 {
			return err
		}
		c.login.invalidate()
		if err := c.login.ensure(ctx, c); err != nil {
			return err
		}
	}
	return ErrUnauthorized
}

func (c *APIClient) rawJSON(ctx context.Context, method, path string, body any, output any) (http.Header, error) {
	endpoint, err := c.baseURL.Parse(path)
	if err != nil {
		return nil, err
	}
	var reader io.Reader
	if body != nil {
		encoded, marshalErr := json.Marshal(body)
		if marshalErr != nil {
			return nil, marshalErr
		}
		reader = bytes.NewReader(encoded)
	}
	request, err := http.NewRequestWithContext(ctx, method, endpoint.String(), reader)
	if err != nil {
		return nil, err
	}
	request.Header.Set("Accept", "application/json")
	if body != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	for name, values := range c.headers {
		for _, value := range values {
			request.Header.Add(name, value)
		}
	}
	if c.login != nil {
		if cookie := c.login.cookieHeader(); cookie != "" {
			request.Header.Set("Cookie", cookie)
		}
		if csrf := c.login.csrfHeader(); csrf != "" {
			request.Header.Set("x-api-csrf", csrf)
		}
	}
	response, err := c.client.Do(request)
	if err != nil {
		return nil, fmt.Errorf("feichi %s %s: %w", method, path, err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
		return response.Header, ErrUnauthorized
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		snippet, _ := io.ReadAll(io.LimitReader(response.Body, 1024))
		return response.Header, fmt.Errorf("feichi %s %s: HTTP %d: %s", method, path, response.StatusCode, strings.TrimSpace(string(snippet)))
	}
	encoded, err := io.ReadAll(io.LimitReader(response.Body, 16<<20))
	if err != nil {
		return response.Header, fmt.Errorf("read feichi %s: %w", path, err)
	}
	var status struct {
		Code any `json:"code"`
	}
	if err := json.Unmarshal(encoded, &status); err != nil {
		return response.Header, fmt.Errorf("decode feichi %s: %w", path, err)
	}
	if fmt.Sprint(status.Code) == "401" || fmt.Sprint(status.Code) == "403" {
		return response.Header, ErrUnauthorized
	}
	if output != nil {
		if err := json.Unmarshal(encoded, output); err != nil {
			return response.Header, fmt.Errorf("decode feichi %s: %w", path, err)
		}
	}
	return response.Header, nil
}

func apiCodeError(code any) error {
	if fmt.Sprint(code) == "401" || fmt.Sprint(code) == "403" {
		return ErrUnauthorized
	}
	return fmt.Errorf("feichi API returned code %v", code)
}
