// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0

package reports

import (
	"bytes"
	"context"
	"encoding/csv"
	"fmt"
	"html/template"
	"io"
	"log/slog"
	"mime/multipart"
	"net/http"
	"sort"
	"strings"
	"time"
	_ "time/tzdata" // Embed timezone database

	"github.com/absmach/magistrala/pkg/errors"
	svcerr "github.com/absmach/magistrala/pkg/errors/service"
	pkglog "github.com/absmach/magistrala/pkg/logger"
	"github.com/absmach/magistrala/pkg/transformers/senml"
)

const nanosecondThreshold = float64(10 * time.Second / time.Nanosecond)

type ReportData struct {
	Title         string
	GeneratedTime string
	GeneratedDate string
	Reports       []Report
	Timezone      string
}

func (r *report) generatePDFReport(ctx context.Context, title string, reports []Report, template ReportTemplate, timezone string) ([]byte, error) {
	for i := range reports {
		sort.Slice(reports[i].Messages, func(j, k int) bool {
			return reports[i].Messages[j].Time < reports[i].Messages[k].Time
		})
	}

	loc, err := resolveTimezone(timezone)
	if err != nil {
		r.runInfo <- pkglog.RunInfo{
			Level:   slog.LevelWarn,
			Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
			Details: []slog.Attr{
				slog.String("report_title", title),
				slog.Time("time", time.Now().UTC()),
			},
		}
	}

	now := time.Now().In(loc)
	displayTZ := timezone
	if strings.TrimSpace(displayTZ) == "" {
		displayTZ = "UTC"
	}

	data := ReportData{
		Title:         title,
		GeneratedTime: now.Format("15:04:05"),
		GeneratedDate: now.Format("02 Jan 2006"),
		Reports:       reports,
		Timezone:      displayTZ,
	}

	templateContent := r.defaultTemplate.String()
	if template.String() != "" {
		templateContent = template.String()
	}
	return r.generate(ctx, templateContent, data)
}

func (r *report) generate(ctx context.Context, templateContent string, data ReportData) ([]byte, error) {
	tmpl := template.New("report").Funcs(template.FuncMap{
		"formatTime":  func(t float64) string { return r.formatTimeWithTimezone(t, data.Timezone) },
		"formatValue": formatValue,
		"add":         func(a, b int) int { return a + b },
		"sub":         func(a, b int) int { return a - b },
		"iterate":     func(count int) []int { return makeRange(count) },
		"ge":          func(a, b int) bool { return a >= b },
		"lt":          func(a, b int) bool { return a < b },
		"eq":          func(a, b int) bool { return a == b },
		"div": func(a, b int) int {
			if b == 0 {
				return 0
			}
			return a / b
		},
		"mod": func(a, b int) int {
			if b == 0 {
				return 0
			}
			return a % b
		},
		"getStartRow": getStartRow,
		"getEndRow":   getEndRow,
	})

	tmpl, err := tmpl.Parse(templateContent)
	if err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	var htmlBuf bytes.Buffer
	if err := tmpl.Execute(&htmlBuf, data); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	htmlContent := htmlBuf.String()
	pdfBytes, err := r.htmlToPDF(ctx, htmlContent)
	if err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	return pdfBytes, nil
}

func (r *report) htmlToPDF(ctx context.Context, htmlContent string) ([]byte, error) {
	var requestBody bytes.Buffer
	writer := multipart.NewWriter(&requestBody)

	htmlPart, err := writer.CreateFormFile("files", "index.html")
	if err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	if _, err := htmlPart.Write([]byte(htmlContent)); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	if err := writer.WriteField("marginTop", "0"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	if err := writer.WriteField("marginBottom", "0"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	if err := writer.WriteField("marginLeft", "0"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	if err := writer.WriteField("marginRight", "0"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	if err := writer.WriteField("printBackground", "true"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	if err := writer.WriteField("preferCSSPageSize", "true"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	if err := writer.WriteField("emulatedMediaType", "print"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	if err := writer.WriteField("waitForSelector", "body"); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	if err := writer.Close(); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, &requestBody)
	if err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	defer resp.Body.Close()

	pdfBytes, err := io.ReadAll(resp.Body)
	if err != nil || resp.StatusCode != http.StatusOK {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}
	return pdfBytes, nil
}

func (r *report) formatTimeWithTimezone(t float64, timezone string) string {
	loc, err := resolveTimezone(timezone)
	if err != nil {
		r.runInfo <- pkglog.RunInfo{
			Level:   slog.LevelWarn,
			Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
			Details: []slog.Attr{slog.Time("time", time.Now().UTC())},
		}
	}

	var timeVal time.Time
	switch {
	case t > nanosecondThreshold:
		timeVal = time.Unix(0, int64(t)).In(loc)
	default:
		timeVal = time.Unix(int64(t), 0).In(loc)
	}

	return timeVal.Format("2006-01-02 15:04:05")
}

func formatValue(msg senml.Message) string {
	switch {
	case msg.Value != nil:
		return fmt.Sprintf("%.2f", *msg.Value)
	case msg.StringValue != nil:
		return *msg.StringValue
	case msg.BoolValue != nil:
		return fmt.Sprintf("%t", *msg.BoolValue)
	case msg.DataValue != nil:
		return *msg.DataValue
	default:
		return "N/A"
	}
}

func makeRange(n int) []int {
	result := make([]int, n)
	for i := range result {
		result[i] = i
	}
	return result
}

func getStartRow(pageNum, firstPageRows, continuationPageRows int) int {
	if pageNum == 0 {
		return 0
	}
	return firstPageRows + (pageNum-1)*continuationPageRows
}

func getEndRow(pageNum, firstPageRows, continuationPageRows, totalMessages int) int {
	var end int
	if pageNum == 0 {
		end = firstPageRows
	} else {
		start := firstPageRows + (pageNum-1)*continuationPageRows
		end = start + continuationPageRows
	}

	if end > totalMessages {
		end = totalMessages
	}
	return end
}

func (r *report) generateCSVReport(_ context.Context, title string, reports []Report, timezone string) ([]byte, error) {
	var buf bytes.Buffer
	writer := csv.NewWriter(&buf)

	headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}

	for i, report := range reports {
		if i > 0 {
			if err := writer.Write([]string{""}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
			if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
			if err := writer.Write([]string{""}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
		} else {
			if err := writer.Write([]string{title}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
			if err := writer.Write([]string{""}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
		}

		if err := writer.Write([]string{"Report Information:"}); err != nil {
			return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
		}

		if err := writer.Write([]string{"Name", report.Metric.Name}); err != nil {
			return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
		}

		if report.Metric.ClientID != "" {
			if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
		}
		if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil {
			return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
		}
		if err := writer.Write([]string{""}); err != nil {
			return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
		}

		if err := writer.Write(headers); err != nil {
			return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
		}

		sort.Slice(report.Messages, func(i, j int) bool {
			return report.Messages[i].Time < report.Messages[j].Time
		})

		for _, msg := range report.Messages {
			timeStr := r.formatTimeWithTimezone(msg.Time, timezone)

			var valueStr string
			if msg.Value != nil {
				valueStr = fmt.Sprintf("%.2f", *msg.Value)
			} else if msg.StringValue != nil {
				valueStr = *msg.StringValue
			} else if msg.BoolValue != nil {
				valueStr = fmt.Sprintf("%v", *msg.BoolValue)
			} else if msg.DataValue != nil {
				valueStr = *msg.DataValue
			} else {
				valueStr = "N/A"
			}

			row := []string{
				timeStr,
				valueStr,
				msg.Unit,
				msg.Protocol,
				msg.Subtopic,
			}

			if err := writer.Write(row); err != nil {
				return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
			}
		}
	}

	writer.Flush()
	if err := writer.Error(); err != nil {
		return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
	}

	return buf.Bytes(), nil
}
