package document

import (
	"context"
	"fmt"
	"strings"

	lark "github.com/larksuite/oapi-sdk-go/v3"
	larkdocx "github.com/larksuite/oapi-sdk-go/v3/service/docx/v1"
	larkwiki "github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2"
)

type Document interface {
	GetDocumentByID(ctx context.Context, documentID string) error
	GetDocumentPlainText(ctx context.Context, documentID string) (string, error)
	GetWikiDocumentToken(ctx context.Context, wikiToken string) (string, error)

	// helpers
	NewDefaultDocumentBuilder(docTitle string) (DocumentBuilder, error)
	GenerateURLFromDocumentID(documentID string) string
}

type docCli struct {
	larkCli     *lark.Client
	larkBaseURL string
}

func (dc *docCli) GetDocumentByID(ctx context.Context, documentID string) error {
	req := larkdocx.NewGetDocumentReqBuilder().
		DocumentId(documentID).
		Build()
	resp, err := dc.larkCli.Docx.V1.Document.Get(ctx, req)

	if err != nil {
		return err
	}

	if !resp.Success() {
		return fmt.Errorf("request failed, code: %d, message: %s, requestId: %s", resp.Code, resp.Msg, resp.RequestId())
	}

	return nil
}

func (dc *docCli) GetDocumentPlainText(ctx context.Context, documentID string) (string, error) {
	var builder strings.Builder
	if err := dc.appendBlockChildren(ctx, documentID, documentID, &builder, 0); err != nil {
		return "", err
	}
	return strings.TrimSpace(builder.String()), nil
}

func (dc *docCli) GetWikiDocumentToken(ctx context.Context, wikiToken string) (string, error) {
	req := larkwiki.NewGetNodeSpaceReqBuilder().
		Token(wikiToken).
		ObjType("wiki").
		Build()
	resp, err := dc.larkCli.Wiki.V2.Space.GetNode(ctx, req)
	if err != nil {
		return "", err
	}
	if !resp.Success() {
		return "", fmt.Errorf("get wiki node failed, code: %d, message: %s, requestId: %s", resp.Code, resp.Msg, resp.RequestId())
	}
	if resp.Data == nil || resp.Data.Node == nil || resp.Data.Node.ObjToken == nil {
		return "", fmt.Errorf("wiki node %s has no document token", wikiToken)
	}
	if resp.Data.Node.ObjType != nil && *resp.Data.Node.ObjType != "docx" {
		return "", fmt.Errorf("wiki node %s is %s, only docx is supported", wikiToken, *resp.Data.Node.ObjType)
	}
	return *resp.Data.Node.ObjToken, nil
}

func (dc *docCli) appendBlockChildren(ctx context.Context, documentID string, blockID string, builder *strings.Builder, depth int) error {
	if depth > 20 {
		return nil
	}
	pageToken := ""
	for {
		reqBuilder := larkdocx.NewGetDocumentBlockChildrenReqBuilder().
			DocumentId(documentID).
			BlockId(blockID).
			DocumentRevisionId(-1).
			PageSize(500).
			UserIdType("user_id")
		if pageToken != "" {
			reqBuilder.PageToken(pageToken)
		}
		resp, err := dc.larkCli.Docx.V1.DocumentBlockChildren.Get(ctx, reqBuilder.Build())
		if err != nil {
			return err
		}
		if !resp.Success() {
			return fmt.Errorf("get document block children failed, code: %d, message: %s, requestId: %s", resp.Code, resp.Msg, resp.RequestId())
		}
		if resp.Data == nil {
			return nil
		}
		for _, block := range resp.Data.Items {
			if block.Table != nil {
				if err := dc.appendTable(ctx, documentID, block.Table, builder, depth+1); err != nil {
					return err
				}
				continue
			}
			if line := blockPlainText(block); line != "" {
				builder.WriteString(line)
				builder.WriteString("\n")
			}
			if block.BlockId != nil && (len(block.Children) > 0 || block.Table != nil) {
				if err := dc.appendBlockChildren(ctx, documentID, *block.BlockId, builder, depth+1); err != nil {
					return err
				}
			}
		}
		if resp.Data.HasMore == nil || !*resp.Data.HasMore || resp.Data.PageToken == nil || *resp.Data.PageToken == "" {
			break
		}
		pageToken = *resp.Data.PageToken
	}
	return nil
}

func (dc *docCli) appendTable(ctx context.Context, documentID string, table *larkdocx.Table, builder *strings.Builder, depth int) error {
	if table == nil {
		return nil
	}
	cols := 0
	rows := 0
	if table.Property != nil {
		if table.Property.ColumnSize != nil {
			cols = *table.Property.ColumnSize
		}
		if table.Property.RowSize != nil {
			rows = *table.Property.RowSize
		}
	}
	if cols <= 0 {
		cols = len(table.Cells)
	}

	builder.WriteString("<table>\n")
	for i, cellID := range table.Cells {
		if cols > 0 && i%cols == 0 {
			row := i/cols + 1
			if rows == 0 || row <= rows {
				builder.WriteString(fmt.Sprintf("<row index=\"%d\">\n", row))
			}
		}

		var cellBuilder strings.Builder
		if err := dc.appendBlockChildren(ctx, documentID, cellID, &cellBuilder, depth+1); err != nil {
			return err
		}
		builder.WriteString(fmt.Sprintf("<cell index=\"%d\">%s</cell>\n", i%cols+1, strings.TrimSpace(cellBuilder.String())))

		if cols > 0 && i%cols == cols-1 {
			builder.WriteString("</row>\n")
		}
	}
	if cols > 0 && len(table.Cells)%cols != 0 {
		builder.WriteString("</row>\n")
	}
	builder.WriteString("</table>\n")
	return nil
}

func blockPlainText(block *larkdocx.Block) string {
	switch {
	case block.Page != nil:
		return textPlainText(block.Page)
	case block.Text != nil:
		return textPlainText(block.Text)
	case block.Heading1 != nil:
		return "# " + textPlainText(block.Heading1)
	case block.Heading2 != nil:
		return "## " + textPlainText(block.Heading2)
	case block.Heading3 != nil:
		return "### " + textPlainText(block.Heading3)
	case block.Heading4 != nil:
		return "#### " + textPlainText(block.Heading4)
	case block.Heading5 != nil:
		return "##### " + textPlainText(block.Heading5)
	case block.Heading6 != nil:
		return "###### " + textPlainText(block.Heading6)
	case block.Heading7 != nil:
		return "####### " + textPlainText(block.Heading7)
	case block.Heading8 != nil:
		return "######## " + textPlainText(block.Heading8)
	case block.Heading9 != nil:
		return "######### " + textPlainText(block.Heading9)
	case block.Bullet != nil:
		return "- " + textPlainText(block.Bullet)
	case block.Ordered != nil:
		return "1. " + textPlainText(block.Ordered)
	case block.Code != nil:
		return textPlainText(block.Code)
	case block.Quote != nil:
		return "> " + textPlainText(block.Quote)
	case block.Todo != nil:
		return "- [ ] " + textPlainText(block.Todo)
	default:
		return ""
	}
}

func textPlainText(text *larkdocx.Text) string {
	if text == nil {
		return ""
	}
	parts := make([]string, 0, len(text.Elements))
	for _, elem := range text.Elements {
		switch {
		case elem.TextRun != nil && elem.TextRun.Content != nil:
			parts = append(parts, *elem.TextRun.Content)
		case elem.MentionUser != nil && elem.MentionUser.UserId != nil:
			parts = append(parts, fmt.Sprintf("@lark_user_id:%s", *elem.MentionUser.UserId))
		case elem.MentionDoc != nil && elem.MentionDoc.Title != nil:
			parts = append(parts, *elem.MentionDoc.Title)
		case elem.Reminder != nil:
			parts = append(parts, "[reminder]")
		}
	}
	return strings.TrimSpace(strings.Join(parts, ""))
}

func New(larkCli *lark.Client, larkBaseURL string) Document {
	return &docCli{
		larkCli:     larkCli,
		larkBaseURL: larkBaseURL,
	}
}
