package parser

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"ragflow/internal/common"
	"strings"

	models "ragflow/internal/entity/models"
)

type doclingChunk struct {
	Text  string `json:"text"`
	Chunk *struct {
		Text string `json:"text"`
	} `json:"chunk"`
}

type doclingDocument struct {
	MDContent   string         `json:"md_content"`
	TextContent string         `json:"text_content"`
	JSONContent map[string]any `json:"json_content"`
}

type doclingResult struct {
	Document *doclingDocument `json:"document"`
	Result   *doclingDocument `json:"result"`
}

type doclingResponse struct {
	Document  *doclingDocument  `json:"document"`
	Documents []doclingDocument `json:"documents"`
	Results   []doclingResult   `json:"results"`
}

func parsePDFWithDocling(ctx context.Context, filename string, data []byte, parser *PDFParser) ParseResult {
	if len(data) == 0 {
		return emptyPDFResult(filename)
	}
	serverURL := strings.TrimSpace(parser.DoclingServerURL)
	if serverURL == "" {
		serverURL = strings.TrimSpace(common.GetEnv(common.EnvDoclingServerURL))
	}
	if serverURL == "" {
		return ParseResult{Err: fmt.Errorf("parser: Docling requires docling_server_url or DOCLING_SERVER_URL")}
	}
	apiKey := strings.TrimSpace(parser.DoclingAPIKey)
	if apiKey == "" {
		apiKey = strings.TrimSpace(common.GetEnv(common.EnvDoclingApiKey))
	}

	baseURL := strings.TrimRight(serverURL, "/")
	auth := ""
	if apiKey != "" {
		auth = "Bearer " + apiKey
	}
	encoded := base64.StdEncoding.EncodeToString(data)
	payloads := []struct {
		endpoint string
		body     func() map[string]any
		chunked  bool
	}{
		{
			endpoint: "/v1/convert/source",
			chunked:  true,
			body:     func() map[string]any { return doclingChunkedPayload(filename, encoded, false) },
		},
		{
			endpoint: "/v1alpha/convert/source",
			chunked:  true,
			body:     func() map[string]any { return doclingChunkedPayload(filename, encoded, true) },
		},
		{
			endpoint: "/v1/convert/source",
			chunked:  false,
			body:     func() map[string]any { return doclingStandardPayload(filename, encoded, false) },
		},
		{
			endpoint: "/v1alpha/convert/source",
			chunked:  false,
			body:     func() map[string]any { return doclingStandardPayload(filename, encoded, true) },
		},
	}

	var lastErr error
	for _, candidate := range payloads {
		url := baseURL + candidate.endpoint
		resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(false), url, auth, candidate.body())
		if err != nil {
			lastErr = fmt.Errorf("%s: %w", candidate.endpoint, err)
			continue
		}
		body, readErr := func() ([]byte, error) {
			defer resp.Body.Close()
			return io.ReadAll(resp.Body)
		}()
		if readErr != nil {
			lastErr = fmt.Errorf("%s: read response: %w", candidate.endpoint, readErr)
			continue
		}
		if resp.StatusCode >= 300 {
			if candidate.chunked {
				lastErr = fmt.Errorf("%s: HTTP %d", candidate.endpoint, resp.StatusCode)
				continue
			}
			lastErr = fmt.Errorf("%s: HTTP %d %s", candidate.endpoint, resp.StatusCode, string(body))
			continue
		}
		if candidate.chunked {
			if res, ok := parseDoclingChunkedResult(filename, body, parser.OutputFormat); ok {
				return res
			}
			lastErr = fmt.Errorf("%s: chunked response contained no usable text", candidate.endpoint)
			continue
		}
		if res, ok := parseDoclingStandardResult(ctx, filename, body, parser.OutputFormat); ok {
			return res
		}
		lastErr = fmt.Errorf("%s: standard response contained no parsed document", candidate.endpoint)
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("Docling remote convert failed")
	}
	return ParseResult{Err: fmt.Errorf("parser: Docling convert: %w", lastErr)}
}

func doclingStandardPayload(filename string, encoded string, alpha bool) map[string]any {
	source := map[string]any{"filename": filename, "base64_string": encoded}
	options := map[string]any{"from_formats": []string{"pdf"}, "to_formats": []string{"json", "md", "text"}}
	if alpha {
		return map[string]any{
			"options":      options,
			"file_sources": []map[string]any{source},
		}
	}
	source["kind"] = "file"
	return map[string]any{
		"options": options,
		"sources": []map[string]any{source},
	}
}

func doclingChunkedPayload(filename string, encoded string, alpha bool) map[string]any {
	payload := doclingStandardPayload(filename, encoded, alpha)
	payload["options"] = map[string]any{
		"from_formats": []string{"pdf"},
		"to_formats":   []string{"json", "md", "text"},
		"do_chunking":  true,
		"chunking_options": map[string]any{
			"max_tokens": 512,
			"overlap":    50,
			"tokenizer":  "sentencepiece",
		},
	}
	return payload
}

func parseDoclingChunkedResult(filename string, body []byte, outputFormat string) (ParseResult, bool) {
	var chunks []doclingChunk
	if err := json.Unmarshal(body, &chunks); err != nil {
		var wrapped struct {
			Results []doclingChunk `json:"results"`
		}
		if err := json.Unmarshal(body, &wrapped); err != nil {
			return ParseResult{}, false
		}
		chunks = wrapped.Results
	}
	texts := make([]string, 0, len(chunks))
	for _, chunk := range chunks {
		text := strings.TrimSpace(chunk.Text)
		if text == "" && chunk.Chunk != nil {
			text = strings.TrimSpace(chunk.Chunk.Text)
		}
		if text != "" {
			texts = append(texts, text)
		}
	}
	if len(texts) == 0 {
		return ParseResult{}, false
	}
	pageCount := 0
	if len(texts) > 0 {
		pageCount = len(texts)
	}
	return doclingTextsToResult(filename, texts, outputFormat, pageCount), true
}

func parseDoclingStandardResult(ctx context.Context, filename string, body []byte, outputFormat string) (ParseResult, bool) {
	var payload doclingResponse
	if err := json.Unmarshal(body, &payload); err != nil {
		return ParseResult{}, false
	}
	docs := make([]doclingDocument, 0, 1+len(payload.Documents)+len(payload.Results))
	if payload.Document != nil {
		docs = append(docs, *payload.Document)
	}
	docs = append(docs, payload.Documents...)
	for _, result := range payload.Results {
		switch {
		case result.Document != nil:
			docs = append(docs, *result.Document)
		case result.Result != nil:
			docs = append(docs, *result.Result)
		}
	}
	for _, doc := range docs {
		if md := strings.TrimSpace(doc.MDContent); md != "" {
			return parseMinerUMarkdownResult(ctx, filename, md, outputFormat, len(docs)), true
		}
		if txt := strings.TrimSpace(doc.TextContent); txt != "" {
			return doclingTextsToResult(filename, []string{txt}, outputFormat, len(docs)), true
		}
		if md, _ := doc.JSONContent["md_content"].(string); strings.TrimSpace(md) != "" {
			return parseMinerUMarkdownResult(ctx, filename, md, outputFormat, len(docs)), true
		}
	}
	return ParseResult{}, false
}

func doclingTextsToResult(filename string, texts []string, outputFormat string, pageCount int) ParseResult {
	fileMeta := pdfFileMeta(filename, pageCount)
	switch strings.ToLower(strings.TrimSpace(outputFormat)) {
	case "", "json":
		items := make([]map[string]any, 0, len(texts))
		for _, text := range texts {
			items = append(items, map[string]any{
				"text":         text,
				"doc_type_kwd": "text",
			})
		}
		return ParseResult{
			OutputFormat: "json",
			File:         fileMeta,
			JSON:         items,
		}
	case "markdown":
		return ParseResult{
			OutputFormat: "markdown",
			File:         fileMeta,
			Markdown:     strings.Join(texts, "\n\n"),
		}
	default:
		return ParseResult{Err: fmt.Errorf("parser: unsupported PDF output_format %q", outputFormat)}
	}
}
