package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"strings"
	"text/tabwriter"

	"gitlab.com/mbugroup/lti-api.git/internal/config"
	"gitlab.com/mbugroup/lti-api.git/internal/database"
	"gorm.io/gorm"
)

const (
	outputModeTable = "table"
	outputModeJSON  = "json"

	reportUsage      = "usage"
	reportWarehouses = "warehouses"
)

type options struct {
	Output               string
	Report               string
	AreaName             string
	KandangLocationName  string
	WrongWarehouseName   string
	CorrectWarehouseName string
	UsableType           string
	DBSSLMode            string
}

type usageRow struct {
	UsableType            string  `gorm:"column:usable_type" json:"usable_type"`
	UsableID              uint    `gorm:"column:usable_id" json:"usable_id"`
	AreaName              string  `gorm:"column:area_name" json:"area_name"`
	LokasiName            string  `gorm:"column:lokasi_name" json:"lokasi_name"`
	KandangName           string  `gorm:"column:kandang_name" json:"kandang_name"`
	WrongWarehouseID      uint    `gorm:"column:wrong_warehouse_id" json:"wrong_warehouse_id"`
	WrongWarehouseName    string  `gorm:"column:wrong_warehouse_name" json:"wrong_warehouse_name"`
	CorrectWarehouseID    uint    `gorm:"column:correct_warehouse_id" json:"correct_warehouse_id"`
	CorrectWarehouseName  string  `gorm:"column:correct_warehouse_name" json:"correct_warehouse_name"`
	ProductNames          string  `gorm:"column:product_names" json:"product_names"`
	SourcePurchaseNumbers string  `gorm:"column:source_purchase_numbers" json:"source_purchase_numbers"`
	SourcePurchaseItemIDs string  `gorm:"column:source_purchase_item_ids" json:"source_purchase_item_ids"`
	QtyFromWrongStock     float64 `gorm:"column:qty_from_wrong_stock" json:"qty_from_wrong_stock"`
	RecordingID           *uint   `gorm:"column:recording_id" json:"recording_id,omitempty"`
	RecordingDate         *string `gorm:"column:recording_date" json:"recording_date,omitempty"`
	SoNumber              *string `gorm:"column:so_number" json:"so_number,omitempty"`
	SoDate                *string `gorm:"column:so_date" json:"so_date,omitempty"`
}

type warehouseMismatchRow struct {
	AreaName             string `gorm:"column:area_name" json:"area_name"`
	WrongLocationName    string `gorm:"column:wrong_location_name" json:"wrong_location_name"`
	KandangLocationName  string `gorm:"column:kandang_location_name" json:"kandang_location_name"`
	KandangID            uint   `gorm:"column:kandang_id" json:"kandang_id"`
	KandangName          string `gorm:"column:kandang_name" json:"kandang_name"`
	WrongWarehouseID     uint   `gorm:"column:wrong_warehouse_id" json:"wrong_warehouse_id"`
	WrongWarehouseName   string `gorm:"column:wrong_warehouse_name" json:"wrong_warehouse_name"`
	WrongWarehouseType   string `gorm:"column:wrong_warehouse_type" json:"wrong_warehouse_type"`
	CorrectWarehouseID   uint   `gorm:"column:correct_warehouse_id" json:"correct_warehouse_id"`
	CorrectWarehouseName string `gorm:"column:correct_warehouse_name" json:"correct_warehouse_name"`
}

type summary struct {
	Rows     int     `json:"rows"`
	TotalQty float64 `json:"total_qty,omitempty"`
}

func main() {
	opts, err := parseFlags()
	if err != nil {
		log.Fatalf("invalid flags: %v", err)
	}

	ctx := context.Background()
	if opts.DBSSLMode != "" {
		config.DBSSLMode = opts.DBSSLMode
	}
	db := database.Connect(config.DBHost, config.DBName)

	switch opts.Report {
	case reportUsage:
		rows, err := loadUsageRows(ctx, db, opts)
		if err != nil {
			log.Fatalf("failed loading usage rows: %v", err)
		}
		renderUsageReport(opts.Output, rows)
	case reportWarehouses:
		rows, err := loadWarehouseMismatchRows(ctx, db, opts)
		if err != nil {
			log.Fatalf("failed loading warehouse mismatch rows: %v", err)
		}
		renderWarehouseReport(opts.Output, rows)
	default:
		log.Fatalf("unsupported --report=%s", opts.Report)
	}
}

func parseFlags() (*options, error) {
	var opts options
	flag.StringVar(&opts.Output, "output", outputModeTable, "Output format: table or json")
	flag.StringVar(&opts.Report, "report", reportUsage, "Report type: usage or warehouses")
	flag.StringVar(&opts.AreaName, "area-name", "", "Optional exact area name filter")
	flag.StringVar(&opts.KandangLocationName, "kandang-location-name", "", "Optional exact canonical kandang location filter")
	flag.StringVar(&opts.WrongWarehouseName, "wrong-warehouse-name", "", "Optional exact wrong warehouse name filter")
	flag.StringVar(&opts.CorrectWarehouseName, "correct-warehouse-name", "", "Optional exact correct warehouse name filter")
	flag.StringVar(&opts.UsableType, "usable-type", "", "Optional usage type filter: RECORDING_STOCK or MARKETING_DELIVERY")
	flag.StringVar(&opts.DBSSLMode, "db-sslmode", "", "Optional database sslmode override, for example: require")
	flag.Parse()

	opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
	opts.Report = strings.ToLower(strings.TrimSpace(opts.Report))
	opts.AreaName = strings.TrimSpace(opts.AreaName)
	opts.KandangLocationName = strings.TrimSpace(opts.KandangLocationName)
	opts.WrongWarehouseName = strings.TrimSpace(opts.WrongWarehouseName)
	opts.CorrectWarehouseName = strings.TrimSpace(opts.CorrectWarehouseName)
	opts.UsableType = strings.ToUpper(strings.TrimSpace(opts.UsableType))
	opts.DBSSLMode = strings.TrimSpace(opts.DBSSLMode)

	if opts.Output == "" {
		opts.Output = outputModeTable
	}
	if opts.Output != outputModeTable && opts.Output != outputModeJSON {
		return nil, fmt.Errorf("unsupported --output=%s", opts.Output)
	}
	if opts.Report == "" {
		opts.Report = reportUsage
	}
	if opts.Report != reportUsage && opts.Report != reportWarehouses {
		return nil, fmt.Errorf("unsupported --report=%s", opts.Report)
	}
	if opts.UsableType != "" && opts.UsableType != "RECORDING_STOCK" && opts.UsableType != "MARKETING_DELIVERY" {
		return nil, fmt.Errorf("unsupported --usable-type=%s", opts.UsableType)
	}

	return &opts, nil
}

func loadUsageRows(ctx context.Context, db *gorm.DB, opts *options) ([]usageRow, error) {
	warehouseFilters, warehouseArgs := buildWarehouseFilters(opts)

	usageFilters := make([]string, 0, 1)
	usageArgs := make([]any, 0, 1)
	if opts.UsableType != "" {
		usageFilters = append(usageFilters, "sa.usable_type = ?")
		usageArgs = append(usageArgs, opts.UsableType)
	}

	args := append([]any{}, warehouseArgs...)
	args = append(args, usageArgs...)

	query := fmt.Sprintf(`
WITH wrong_warehouses AS (
	SELECT
		w.id AS wrong_warehouse_id,
		w.name AS wrong_warehouse_name,
		k.id AS kandang_id,
		k.name AS kandang_name,
		a.name AS area_name,
		kl.name AS kandang_location_name,
		correct_w.id AS correct_warehouse_id,
		correct_w.name AS correct_warehouse_name
	FROM warehouses w
	JOIN kandangs k
		ON k.id = w.kandang_id
	   AND k.deleted_at IS NULL
	JOIN locations kl
		ON kl.id = k.location_id
	JOIN areas a
		ON a.id = kl.area_id
	JOIN LATERAL (
		SELECT w2.id, w2.name
		FROM warehouses w2
		WHERE w2.kandang_id = w.kandang_id
		  AND w2.location_id = k.location_id
		  AND w2.deleted_at IS NULL
		  AND w2.id <> w.id
		ORDER BY w2.id ASC
		LIMIT 1
	) AS correct_w ON TRUE
	WHERE w.deleted_at IS NULL
	  AND w.kandang_id IS NOT NULL
	  AND w.location_id IS DISTINCT FROM k.location_id
	  %s
),
wrong_allocs AS (
	SELECT
		sa.usable_type,
		sa.usable_id,
		sa.qty,
		pi.id AS purchase_item_id,
		COALESCE(p.po_number, p.pr_number) AS purchase_number,
		pr.name AS product_name,
		ww.area_name,
		ww.kandang_location_name,
		ww.kandang_name,
		ww.wrong_warehouse_id,
		ww.wrong_warehouse_name,
		ww.correct_warehouse_id,
		ww.correct_warehouse_name
	FROM stock_allocations sa
	JOIN purchase_items pi
		ON pi.id = sa.stockable_id
	JOIN purchases p
		ON p.id = pi.purchase_id
	   AND p.deleted_at IS NULL
	JOIN products pr
		ON pr.id = pi.product_id
	JOIN wrong_warehouses ww
		ON ww.wrong_warehouse_id = pi.warehouse_id
	WHERE sa.stockable_type = 'PURCHASE_ITEMS'
	  AND sa.status = 'ACTIVE'
	  AND sa.allocation_purpose = 'CONSUME'
	  AND sa.deleted_at IS NULL
	  %s
)
SELECT
	wa.usable_type,
	wa.usable_id,
	wa.area_name,
	wa.kandang_location_name AS lokasi_name,
	wa.kandang_name,
	wa.wrong_warehouse_id,
	wa.wrong_warehouse_name,
	wa.correct_warehouse_id,
	wa.correct_warehouse_name,
	STRING_AGG(DISTINCT wa.product_name, ' | ') AS product_names,
	STRING_AGG(DISTINCT wa.purchase_number, ', ') AS source_purchase_numbers,
	STRING_AGG(DISTINCT wa.purchase_item_id::text, ', ') AS source_purchase_item_ids,
	SUM(wa.qty) AS qty_from_wrong_stock,
	rs.recording_id,
	TO_CHAR(r.record_datetime::date, 'YYYY-MM-DD') AS recording_date,
	m.so_number,
	TO_CHAR(m.so_date::date, 'YYYY-MM-DD') AS so_date
FROM wrong_allocs wa
LEFT JOIN recording_stocks rs
	ON wa.usable_type = 'RECORDING_STOCK'
   AND rs.id = wa.usable_id
LEFT JOIN recordings r
	ON r.id = rs.recording_id
LEFT JOIN marketing_delivery_products mdp
	ON wa.usable_type = 'MARKETING_DELIVERY'
   AND mdp.id = wa.usable_id
LEFT JOIN marketing_products mp
	ON mp.id = mdp.marketing_product_id
LEFT JOIN marketings m
	ON m.id = mp.marketing_id
GROUP BY
	wa.usable_type,
	wa.usable_id,
	wa.area_name,
	wa.kandang_location_name,
	wa.kandang_name,
	wa.wrong_warehouse_id,
	wa.wrong_warehouse_name,
	wa.correct_warehouse_id,
	wa.correct_warehouse_name,
	rs.recording_id,
	r.record_datetime,
	m.so_number,
	m.so_date
ORDER BY
	wa.area_name ASC,
	wa.kandang_location_name ASC,
	wa.wrong_warehouse_name ASC,
	wa.usable_type ASC,
	wa.usable_id ASC
`, andClause(warehouseFilters), andClause(usageFilters))

	rows := make([]usageRow, 0)
	if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func loadWarehouseMismatchRows(ctx context.Context, db *gorm.DB, opts *options) ([]warehouseMismatchRow, error) {
	warehouseFilters, args := buildWarehouseFilters(opts)

	query := fmt.Sprintf(`
SELECT
	a.name AS area_name,
	wl.name AS wrong_location_name,
	kl.name AS kandang_location_name,
	k.id AS kandang_id,
	k.name AS kandang_name,
	w.id AS wrong_warehouse_id,
	w.name AS wrong_warehouse_name,
	w.type AS wrong_warehouse_type,
	correct_w.id AS correct_warehouse_id,
	correct_w.name AS correct_warehouse_name
FROM warehouses w
JOIN kandangs k
	ON k.id = w.kandang_id
   AND k.deleted_at IS NULL
JOIN locations kl
	ON kl.id = k.location_id
JOIN areas a
	ON a.id = kl.area_id
LEFT JOIN locations wl
	ON wl.id = w.location_id
JOIN LATERAL (
	SELECT w2.id, w2.name
	FROM warehouses w2
	WHERE w2.kandang_id = w.kandang_id
	  AND w2.location_id = k.location_id
	  AND w2.deleted_at IS NULL
	  AND w2.id <> w.id
	ORDER BY w2.id ASC
	LIMIT 1
) AS correct_w ON TRUE
WHERE w.deleted_at IS NULL
  AND w.kandang_id IS NOT NULL
  AND w.location_id IS DISTINCT FROM k.location_id
  %s
ORDER BY a.name ASC, kl.name ASC, k.name ASC, w.id ASC
`, andClause(warehouseFilters))

	rows := make([]warehouseMismatchRow, 0)
	if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func buildWarehouseFilters(opts *options) ([]string, []any) {
	filters := make([]string, 0, 4)
	args := make([]any, 0, 4)

	if opts == nil {
		return filters, args
	}
	if opts.AreaName != "" {
		filters = append(filters, "a.name = ?")
		args = append(args, opts.AreaName)
	}
	if opts.KandangLocationName != "" {
		filters = append(filters, "kl.name = ?")
		args = append(args, opts.KandangLocationName)
	}
	if opts.WrongWarehouseName != "" {
		filters = append(filters, "w.name = ?")
		args = append(args, opts.WrongWarehouseName)
	}
	if opts.CorrectWarehouseName != "" {
		filters = append(filters, "correct_w.name = ?")
		args = append(args, opts.CorrectWarehouseName)
	}

	return filters, args
}

func andClause(filters []string) string {
	if len(filters) == 0 {
		return ""
	}
	return " AND " + strings.Join(filters, " AND ")
}

func renderUsageReport(mode string, rows []usageRow) {
	if mode == outputModeJSON {
		payload := map[string]any{
			"report":  reportUsage,
			"rows":    rows,
			"summary": summarizeUsage(rows),
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		_ = enc.Encode(payload)
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
	fmt.Fprintln(w, "USABLE_TYPE\tUSABLE_ID\tAREA\tLOKASI\tKANDANG\tWRONG_WAREHOUSE\tCORRECT_WAREHOUSE\tPRODUCTS\tQTY_FROM_WRONG_STOCK\tRECORDING_ID\tRECORDING_DATE\tSO_NUMBER\tSO_DATE\tSOURCE_PURCHASES\tSOURCE_PURCHASE_ITEM_IDS")
	for _, row := range rows {
		fmt.Fprintf(
			w,
			"%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%.3f\t%s\t%s\t%s\t%s\t%s\t%s\n",
			row.UsableType,
			row.UsableID,
			row.AreaName,
			row.LokasiName,
			row.KandangName,
			row.WrongWarehouseName,
			row.CorrectWarehouseName,
			row.ProductNames,
			row.QtyFromWrongStock,
			displayOptionalUint(row.RecordingID),
			displayOptionalString(row.RecordingDate),
			displayOptionalString(row.SoNumber),
			displayOptionalString(row.SoDate),
			row.SourcePurchaseNumbers,
			row.SourcePurchaseItemIDs,
		)
	}
	_ = w.Flush()

	s := summarizeUsage(rows)
	fmt.Printf("\nSummary: rows=%d total_qty=%.3f\n", s.Rows, s.TotalQty)
}

func renderWarehouseReport(mode string, rows []warehouseMismatchRow) {
	if mode == outputModeJSON {
		payload := map[string]any{
			"report":  reportWarehouses,
			"rows":    rows,
			"summary": summary{Rows: len(rows)},
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		_ = enc.Encode(payload)
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
	fmt.Fprintln(w, "AREA\tKANDANG_LOCATION\tKANDANG_ID\tKANDANG\tWRONG_LOCATION\tWRONG_WAREHOUSE_ID\tWRONG_WAREHOUSE\tWRONG_WAREHOUSE_TYPE\tCORRECT_WAREHOUSE_ID\tCORRECT_WAREHOUSE")
	for _, row := range rows {
		fmt.Fprintf(
			w,
			"%s\t%s\t%d\t%s\t%s\t%d\t%s\t%s\t%d\t%s\n",
			row.AreaName,
			row.KandangLocationName,
			row.KandangID,
			row.KandangName,
			row.WrongLocationName,
			row.WrongWarehouseID,
			row.WrongWarehouseName,
			row.WrongWarehouseType,
			row.CorrectWarehouseID,
			row.CorrectWarehouseName,
		)
	}
	_ = w.Flush()

	fmt.Printf("\nSummary: rows=%d\n", len(rows))
}

func summarizeUsage(rows []usageRow) summary {
	out := summary{Rows: len(rows)}
	for _, row := range rows {
		out.TotalQty += row.QtyFromWrongStock
	}
	return out
}

func displayOptionalUint(value *uint) string {
	if value == nil || *value == 0 {
		return "-"
	}
	return fmt.Sprintf("%d", *value)
}

func displayOptionalString(value *string) string {
	if value == nil || strings.TrimSpace(*value) == "" {
		return "-"
	}
	return *value
}
