package sheets

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"unicode"

	lark "github.com/larksuite/oapi-sdk-go/v3"
	larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
	larksheets "github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3"
)

type Sheets interface {
	CreateSpreadsheet(ctx context.Context, title string, folderToken string) (*Spreadsheet, error)
	WriteRange(ctx context.Context, spreadsheetToken string, sheetID string, startCell string, values [][]string) error
}

type Spreadsheet struct {
	Token   string
	SheetID string
	URL     string
}

type client struct {
	larkCli *lark.Client
}

func New(larkCli *lark.Client) Sheets {
	return &client{larkCli: larkCli}
}

func (c *client) CreateSpreadsheet(ctx context.Context, title string, folderToken string) (*Spreadsheet, error) {
	builder := larksheets.NewSpreadsheetBuilder().Title(title)
	if folderToken != "" {
		builder.FolderToken(folderToken)
	}
	req := larksheets.NewCreateSpreadsheetReqBuilder().
		Spreadsheet(builder.Build()).
		Build()
	resp, err := c.larkCli.Sheets.V3.Spreadsheet.Create(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("create lark spreadsheet failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("create lark spreadsheet failed: %s", resp.CodeError.String())
	}
	if resp.Data == nil || resp.Data.Spreadsheet == nil || resp.Data.Spreadsheet.SpreadsheetToken == nil {
		return nil, fmt.Errorf("create lark spreadsheet failed: missing spreadsheet token")
	}

	token := *resp.Data.Spreadsheet.SpreadsheetToken
	sheetID, err := c.firstSheetID(ctx, token)
	if err != nil {
		return nil, err
	}

	url := ""
	if resp.Data.Spreadsheet.Url != nil {
		url = *resp.Data.Spreadsheet.Url
	}
	return &Spreadsheet{Token: token, SheetID: sheetID, URL: url}, nil
}

func (c *client) firstSheetID(ctx context.Context, spreadsheetToken string) (string, error) {
	req := larksheets.NewQuerySpreadsheetSheetReqBuilder().
		SpreadsheetToken(spreadsheetToken).
		Build()
	resp, err := c.larkCli.Sheets.V3.SpreadsheetSheet.Query(ctx, req)
	if err != nil {
		return "", fmt.Errorf("query lark spreadsheet sheets failed: %w", err)
	}
	if !resp.Success() {
		return "", fmt.Errorf("query lark spreadsheet sheets failed: %s", resp.CodeError.String())
	}
	if resp.Data == nil || len(resp.Data.Sheets) == 0 || resp.Data.Sheets[0].SheetId == nil {
		return "", fmt.Errorf("query lark spreadsheet sheets failed: missing sheet id")
	}
	return *resp.Data.Sheets[0].SheetId, nil
}

func (c *client) WriteRange(ctx context.Context, spreadsheetToken string, sheetID string, startCell string, values [][]string) error {
	writeRange, err := buildWriteRange(sheetID, startCell, values)
	if err != nil {
		return err
	}
	body := map[string]any{
		"valueRange": map[string]any{
			"range":  writeRange,
			"values": values,
		},
	}

	resp, err := c.larkCli.Put(
		ctx,
		fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/values", spreadsheetToken),
		body,
		larkcore.AccessTokenTypeTenant,
	)
	if err != nil {
		return fmt.Errorf("write lark spreadsheet range %s failed: %w", writeRange, err)
	}

	respErr := larkcore.CodeError{}
	if err := json.Unmarshal(resp.RawBody, &respErr); err != nil {
		return fmt.Errorf("unmarshal lark spreadsheet write response failed: %w", err)
	}
	if respErr.Code != 0 {
		return fmt.Errorf("write lark spreadsheet range %s failed: %s", writeRange, respErr.String())
	}
	return nil
}

func buildWriteRange(sheetID string, startCell string, values [][]string) (string, error) {
	if strings.TrimSpace(sheetID) == "" {
		return "", fmt.Errorf("sheet id is required")
	}
	startCol, startRow, err := parseCell(startCell)
	if err != nil {
		return "", err
	}
	maxCols := 0
	for _, row := range values {
		if len(row) > maxCols {
			maxCols = len(row)
		}
	}
	if len(values) == 0 || maxCols == 0 {
		return "", fmt.Errorf("values must contain at least one row and one column")
	}

	endCol := startCol + maxCols - 1
	endRow := startRow + len(values) - 1
	return fmt.Sprintf("%s!%s%d:%s%d", sheetID, columnNumberToName(startCol), startRow, columnNumberToName(endCol), endRow), nil
}

func parseCell(cell string) (int, int, error) {
	cell = strings.TrimSpace(strings.ToUpper(cell))
	if cell == "" {
		return 0, 0, fmt.Errorf("start cell is required")
	}

	i := 0
	for i < len(cell) && unicode.IsLetter(rune(cell[i])) {
		i++
	}
	if i == 0 || i == len(cell) {
		return 0, 0, fmt.Errorf("invalid start cell %q", cell)
	}

	col := 0
	for _, r := range cell[:i] {
		col = col*26 + int(r-'A') + 1
	}
	row, err := strconv.Atoi(cell[i:])
	if err != nil || row <= 0 {
		return 0, 0, fmt.Errorf("invalid start cell %q", cell)
	}
	return col, row, nil
}

func columnNumberToName(col int) string {
	var out []byte
	for col > 0 {
		col--
		out = append([]byte{byte('A' + col%26)}, out...)
		col /= 26
	}
	return string(out)
}
