package handlers

import (
	"archive/zip"
	"bytes"
	"context"
	"errors"
	"io"
	"log/slog"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	filemanager "wucher/internal/domain/file_manager"
)

var (
	xlsxVMLStyleAttrRE   = regexp.MustCompile(`(?i)(style\s*=\s*["'])([^"']*)(["'])`)
	xlsxVMLLengthTokenRE = regexp.MustCompile(`^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:pt|px|in|cm|mm|pc|em|ex|%)$`)
	xlsxVMLShapeTagRE    = regexp.MustCompile(`(?is)<v:shape\b[^>]*>`)
	xlsxVMLShapeIDAttrRE = regexp.MustCompile(`(?i)(\bid\s*=\s*["'])(_x0000_s\d+)(["'])`)
	xlsxVMLShapeIDNumRE  = regexp.MustCompile(`(?i)^_x0000_s(\d+)$`)
	xlsxVMLShapeTypeRE   = regexp.MustCompile(`(?i)<v:shapetype\b`)
	xlsxVMLNoteTypeRefRE = regexp.MustCompile(`(?i)type\s*=\s*["']#_x0000_t202["']`)
	xlsxVMLRootTagRE     = regexp.MustCompile(`(?is)<xml\b[^>]*>`)
)

// xlsxVMLNoteShapeTypeHeader is the standard <o:shapelayout>/<v:shapetype>
// preamble that Excel writes at the top of every comment/note VML drawing.
// Some producers (and files that have been round-tripped through other tools)
// omit it while still emitting shapes that reference type="#_x0000_t202";
// LibreOffice/Collabora then flags the package as corrupt and offers a repair.
// Injecting the missing definition makes the shape reference resolve and
// removes the repair prompt.
const xlsxVMLNoteShapeTypeHeader = `<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>` +
	`<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">` +
	`<v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>`

type xlsxSanitizerStorage interface {
	GetObject(context.Context, string) (io.ReadCloser, filemanager.ObjectMetadata, error)
	PutObject(context.Context, filemanager.PutObjectInput) (filemanager.ObjectMetadata, error)
}

type xlsxSanitizerFileUpdater interface {
	UpdateFile(context.Context, *filemanager.File) error
}

func sanitizeXLSXPayload(payload []byte) ([]byte, int, bool, error) {
	reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
	if err != nil {
		return nil, 0, false, errors.New("file content is not a valid office zip document")
	}

	var buf bytes.Buffer
	writer := zip.NewWriter(&buf)
	if reader.Comment != "" {
		if err := writer.SetComment(reader.Comment); err != nil {
			_ = writer.Close()
			return nil, 0, false, err
		}
	}

	totalStyleFixes := 0
	changed := false

	for _, file := range reader.File {
		header := file.FileHeader
		header.Name = file.Name
		header.Comment = file.Comment
		header.NonUTF8 = file.NonUTF8

		if file.FileInfo().IsDir() {
			entry, err := writer.CreateHeader(&header)
			if err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			if _, err := entry.Write(nil); err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			continue
		}

		if !isVMLDrawingEntry(file.Name) {
			raw, err := file.OpenRaw()
			if err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			entry, err := writer.CreateRaw(&header)
			if err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			if _, err := io.Copy(entry, raw); err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			continue
		}

		rc, err := file.Open()
		if err != nil {
			_ = writer.Close()
			return nil, 0, false, err
		}
		filePayload, readErr := io.ReadAll(rc)
		_ = rc.Close()
		if readErr != nil {
			_ = writer.Close()
			return nil, 0, false, readErr
		}

		sanitizedPayload, styleFixes, fileChanged, err := sanitizeVMLDrawingPayload(filePayload)
		if err != nil {
			_ = writer.Close()
			return nil, 0, false, err
		}
		if dedupedPayload, idFixes, idChanged := dedupeVMLShapeIDs(sanitizedPayload); idChanged {
			sanitizedPayload = dedupedPayload
			fileChanged = true
			totalStyleFixes += idFixes
		}
		if shapeTypePayload, shapeTypeFixes, shapeTypeChanged := ensureVMLNoteShapeType(sanitizedPayload); shapeTypeChanged {
			sanitizedPayload = shapeTypePayload
			fileChanged = true
			totalStyleFixes += shapeTypeFixes
		}
		if fileChanged {
			changed = true
			totalStyleFixes += styleFixes
		}

		if !fileChanged {
			raw, err := file.OpenRaw()
			if err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			entry, err := writer.CreateRaw(&header)
			if err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			if _, err := io.Copy(entry, raw); err != nil {
				_ = writer.Close()
				return nil, 0, false, err
			}
			continue
		}

		entry, err := writer.CreateHeader(&header)
		if err != nil {
			_ = writer.Close()
			return nil, 0, false, err
		}
		if _, err := entry.Write(sanitizedPayload); err != nil {
			_ = writer.Close()
			return nil, 0, false, err
		}
	}

	if err := writer.Close(); err != nil {
		return nil, 0, false, err
	}
	if !changed {
		return payload, 0, false, nil
	}
	return buf.Bytes(), totalStyleFixes, true, nil
}

func sanitizeVMLDrawingPayload(payload []byte) ([]byte, int, bool, error) {
	totalFixes := 0
	changed := false

	rewritten := xlsxVMLStyleAttrRE.ReplaceAllFunc(payload, func(match []byte) []byte {
		sub := xlsxVMLStyleAttrRE.FindSubmatch(match)
		if len(sub) != 4 {
			return match
		}

		normalized, styleFixes, fileChanged := sanitizeVMLStyleValue(string(sub[2]))
		if !fileChanged {
			return match
		}

		totalFixes += styleFixes
		changed = true
		out := make([]byte, 0, len(sub[1])+len(normalized)+len(sub[3]))
		out = append(out, sub[1]...)
		out = append(out, normalized...)
		out = append(out, sub[3]...)
		return out
	})

	if !changed {
		return payload, 0, false, nil
	}
	return rewritten, totalFixes, true, nil
}

func dedupeVMLShapeIDs(payload []byte) ([]byte, int, bool) {
	if len(payload) == 0 {
		return payload, 0, false
	}

	source := string(payload)
	tags := xlsxVMLShapeTagRE.FindAllStringIndex(source, -1)
	if len(tags) == 0 {
		return payload, 0, false
	}

	maxID := 0
	for _, match := range xlsxVMLShapeIDAttrRE.FindAllStringSubmatch(source, -1) {
		if len(match) < 3 {
			continue
		}
		if sub := xlsxVMLShapeIDNumRE.FindStringSubmatch(match[2]); len(sub) == 2 {
			if n, err := strconv.Atoi(sub[1]); err == nil && n > maxID {
				maxID = n
			}
		}
	}
	if maxID == 0 {
		return payload, 0, false
	}

	seen := make(map[string]struct{}, len(tags))
	var buf strings.Builder
	buf.Grow(len(source) + len(tags)*8)
	last := 0
	nextID := maxID + 1
	fixCount := 0

	for _, loc := range tags {
		tag := source[loc[0]:loc[1]]
		newTag, changed := dedupeVMLShapeIDTag(tag, seen, &nextID)
		if changed {
			fixCount++
		}
		buf.WriteString(source[last:loc[0]])
		buf.WriteString(newTag)
		last = loc[1]
	}
	buf.WriteString(source[last:])

	if fixCount == 0 {
		return payload, 0, false
	}
	return []byte(buf.String()), fixCount, true
}

func dedupeVMLShapeIDTag(tag string, seen map[string]struct{}, nextID *int) (string, bool) {
	match := xlsxVMLShapeIDAttrRE.FindStringSubmatchIndex(tag)
	if len(match) < 8 {
		return tag, false
	}
	currentID := tag[match[4]:match[5]]
	if _, ok := seen[currentID]; !ok {
		seen[currentID] = struct{}{}
		return tag, false
	}
	if nextID == nil {
		return tag, false
	}
	newID := "_x0000_s" + strconv.Itoa(*nextID)
	*nextID = *nextID + 1
	return tag[:match[4]] + newID + tag[match[5]:], true
}

// ensureVMLNoteShapeType injects the standard note <v:shapetype> definition
// (and its <o:shapelayout> preamble) when the drawing references
// type="#_x0000_t202" but never defines it. Returns the payload unchanged when
// a shapetype already exists, when no note shape references it, or when the
// root <xml> element lacks the o:/v: namespaces the injected markup needs.
func ensureVMLNoteShapeType(payload []byte) ([]byte, int, bool) {
	if len(payload) == 0 {
		return payload, 0, false
	}
	if xlsxVMLShapeTypeRE.Match(payload) {
		return payload, 0, false
	}
	if !xlsxVMLNoteTypeRefRE.Match(payload) {
		return payload, 0, false
	}

	root := xlsxVMLRootTagRE.FindIndex(payload)
	if root == nil {
		return payload, 0, false
	}
	rootTag := bytes.ToLower(payload[root[0]:root[1]])
	if !bytes.Contains(rootTag, []byte("xmlns:o")) || !bytes.Contains(rootTag, []byte("xmlns:v")) {
		return payload, 0, false
	}

	out := make([]byte, 0, len(payload)+len(xlsxVMLNoteShapeTypeHeader))
	out = append(out, payload[:root[1]]...)
	out = append(out, xlsxVMLNoteShapeTypeHeader...)
	out = append(out, payload[root[1]:]...)
	return out, 1, true
}

func sanitizeVMLStyleValue(raw string) (string, int, bool) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return raw, 0, false
	}

	tokens := strings.Split(trimmed, ";")
	if len(tokens) == 0 {
		return raw, 0, false
	}

	positionIndex := -1
	for i := range tokens {
		if isVMLAbsolutePositionToken(tokens[i]) {
			positionIndex = i
			break
		}
	}
	if positionIndex < 0 {
		return raw, 0, false
	}

	leadingLengths := make([]string, 0, 2)
	nextIndex := positionIndex + 1
	for nextIndex < len(tokens) && len(leadingLengths) < 2 {
		token := strings.TrimSpace(tokens[nextIndex])
		if token == "" {
			nextIndex++
			continue
		}
		if !isVMLLengthToken(token) {
			break
		}
		leadingLengths = append(leadingLengths, token)
		nextIndex++
	}

	if len(leadingLengths) == 0 {
		return raw, 0, false
	}

	remaining := make([]string, 0, len(tokens)-nextIndex+3)
	for ; nextIndex < len(tokens); nextIndex++ {
		token := strings.TrimSpace(tokens[nextIndex])
		if token == "" {
			continue
		}
		remaining = append(remaining, token)
	}

	styleParts := make([]string, 0, len(tokens)+2)
	for i := 0; i < positionIndex; i++ {
		token := strings.TrimSpace(tokens[i])
		if token == "" {
			continue
		}
		styleParts = append(styleParts, token)
	}
	styleParts = append(styleParts, "position:absolute")
	styleParts = append(styleParts, "margin-left:"+leadingLengths[0])
	if len(leadingLengths) >= 2 {
		styleParts = append(styleParts, "margin-top:"+leadingLengths[1])
	} else {
		styleParts = append(styleParts, "margin-top:0pt")
	}
	styleParts = append(styleParts, remaining...)

	return strings.Join(styleParts, ";"), len(leadingLengths), true
}

func isVMLDrawingEntry(name string) bool {
	name = strings.ToLower(strings.TrimSpace(strings.ReplaceAll(name, "\\", "/")))
	if name == "" {
		return false
	}
	return strings.HasPrefix(name, "xl/drawings/vmldrawing") && strings.HasSuffix(name, ".vml")
}

func isVMLAbsolutePositionToken(token string) bool {
	token = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(token), " ", ""))
	return token == "position:absolute"
}

func isVMLLengthToken(token string) bool {
	return xlsxVMLLengthTokenRE.MatchString(strings.ToLower(strings.TrimSpace(token)))
}

func (h *FileManagerFileHandler) sanitizeStoredXLSXObject(ctx context.Context, row *filemanager.File, objectKey, nameHint, mimeHint, operation string) (bool, int64, error) {
	if h == nil || h.storage == nil {
		return false, 0, nil
	}
	storage, ok := any(h.storage).(xlsxSanitizerStorage)
	if !ok {
		return false, 0, errors.New("storage does not support office sanitization")
	}
	return sanitizeStoredXLSXObject(ctx, storage, h.svc, row, objectKey, nameHint, mimeHint, operation)
}

func sanitizeStoredXLSXObject(
	ctx context.Context,
	storage xlsxSanitizerStorage,
	updater xlsxSanitizerFileUpdater,
	row *filemanager.File,
	objectKey,
	nameHint,
	mimeHint,
	operation string,
) (bool, int64, error) {
	if storage == nil {
		return false, 0, nil
	}
	if ctx == nil {
		ctx = context.Background()
	}

	objectKey = strings.TrimSpace(objectKey)
	if objectKey == "" && row != nil {
		objectKey = strings.TrimSpace(row.ObjectKey)
	}
	if objectKey == "" {
		return false, 0, nil
	}

	name := strings.TrimSpace(nameHint)
	if name == "" && row != nil {
		name = strings.TrimSpace(row.Name)
	}
	ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), "."))
	if ext == "" {
		ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(objectKey), "."))
	}
	if ext != "xlsx" {
		return false, 0, nil
	}

	body, meta, err := storage.GetObject(ctx, objectKey)
	if err != nil {
		return false, 0, err
	}
	defer body.Close()

	payload, err := io.ReadAll(body)
	if err != nil {
		return false, 0, err
	}

	sanitizedPayload, styleFixes, changed, err := sanitizeXLSXPayload(payload)
	if err != nil {
		return false, 0, err
	}
	if !changed {
		return false, int64(len(payload)), nil
	}

	contentType := strings.TrimSpace(firstNonEmpty(mimeHint, meta.ContentType, canonicalOfficeContentType(name)))
	if contentType == "" {
		contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
	}

	newMeta, err := storage.PutObject(ctx, filemanager.PutObjectInput{
		ObjectKey:   objectKey,
		ContentType: contentType,
		SizeBytes:   int64(len(sanitizedPayload)),
		Body:        bytes.NewReader(sanitizedPayload),
	})
	if err != nil {
		return true, int64(len(sanitizedPayload)), err
	}

	logArgs := []any{
		slog.String("operation", strings.TrimSpace(operation)),
		slog.String("object_key", objectKey),
		slog.String("mime_type", contentType),
		slog.Int("style_fix_count", styleFixes),
		slog.Int("before_bytes", len(payload)),
		slog.Int("after_bytes", len(sanitizedPayload)),
	}
	if row != nil {
		logArgs = append(logArgs, slog.String("file_id", idString(row.ID)))
		logArgs = append(logArgs, slog.String("file_name", strings.TrimSpace(row.Name)))
	}
	slog.WarnContext(ctx, "sanitized malformed xlsx VML drawing", logArgs...)

	if row != nil {
		row.SizeBytes = int64(len(sanitizedPayload))
		row.MimeType = contentType
		if strings.TrimSpace(newMeta.Bucket) != "" {
			row.Bucket = newMeta.Bucket
		}
		if strings.TrimSpace(newMeta.ObjectKey) != "" {
			row.ObjectKey = newMeta.ObjectKey
		}
		if strings.TrimSpace(newMeta.ETag) != "" {
			row.ETag = newMeta.ETag
		}
		if strings.TrimSpace(newMeta.VersionID) != "" {
			row.ObjectVersion = newMeta.VersionID
		}
		if updater != nil {
			if err := updater.UpdateFile(ctx, row); err != nil {
				return true, int64(len(sanitizedPayload)), err
			}
		}
	}

	return true, int64(len(sanitizedPayload)), nil
}
