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 (
	outputTable = "table"
	outputJSON  = "json"

	caseA   = "A"
	caseB   = "B"
	caseAll = "ALL"
)

type options struct {
	Output              string
	AreaName            string
	KandangLocationName string
	DBSSLMode           string
	VerifyCase          string
}

type sourceWarehouseCheck struct {
	AreaName              string  `json:"area_name"`
	KandangLocationName   string  `json:"kandang_location_name"`
	KandangID             uint    `json:"kandang_id"`
	KandangName           string  `json:"kandang_name"`
	SourceWarehouseID     uint    `json:"source_warehouse_id"`
	SourceWarehouseName   string  `json:"source_warehouse_name"`
	Case                  string  `json:"case" gorm:"column:case_type"`
	DeletedAt             *string `json:"deleted_at"`
	StockInProductWH      float64 `json:"stock_in_product_wh"`
	ActivePurchaseItems   int64   `json:"active_purchase_items"`
	Status                string  `json:"status"`
}

type destinationWarehouseCheck struct {
	AreaName            string  `json:"area_name"`
	KandangLocationName string  `json:"kandang_location_name"`
	FarmWarehouseID     uint    `json:"farm_warehouse_id"`
	FarmWarehouseName   string  `json:"farm_warehouse_name"`
	ProductID           uint    `json:"product_id"`
	ProductName         string  `json:"product_name"`
	CurrentQty          float64 `json:"current_qty"`
	StockLogsTotal      float64 `json:"stock_logs_total"`
	StockLogsCount      int64   `json:"stock_logs_count"`
	Status              string  `json:"status"`
}

type orphanedReferenceCheck struct {
	Table               string `json:"table"`
	Column              string `json:"column"`
	ReferenceCount      int64  `json:"reference_count"`
	DeletedWarehouseIDs string `json:"deleted_warehouse_ids"`
}

type verificationSummary struct {
	TotalSourceWarehouses           int     `json:"total_source_warehouses"`
	CleanSourceWarehouses           int     `json:"clean_source_warehouses"`
	DirtySourceWarehouses           int     `json:"dirty_source_warehouses"`
	TotalDestinationWarehouses      int     `json:"total_destination_warehouses"`
	MatchingDestinations            int     `json:"matching_destinations"`
	DiscrepancyDestinations         int     `json:"discrepancy_destinations"`
	TotalOrphanedReferences         int64   `json:"total_orphaned_references"`
	OverallStatus                   string  `json:"overall_status"`
}

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

	if opts.DBSSLMode != "" {
		config.DBSSLMode = opts.DBSSLMode
	}

	ctx := context.Background()
	db := database.Connect(config.DBHost, config.DBName)

	// Verify source warehouses
	sourceChecks, err := verifySourceWarehouses(ctx, db, opts)
	if err != nil {
		log.Fatalf("failed to verify source warehouses: %v", err)
	}

	// Verify destination warehouses
	destChecks, err := verifyDestinationWarehouses(ctx, db, opts, sourceChecks)
	if err != nil {
		log.Fatalf("failed to verify destination warehouses: %v", err)
	}

	// Verify no orphaned references
	orphanedRefs, err := verifyOrphanedReferences(ctx, db, sourceChecks)
	if err != nil {
		log.Fatalf("failed to verify orphaned references: %v", err)
	}

	// Render results
	summary := buildSummary(sourceChecks, destChecks, orphanedRefs)
	renderVerification(opts.Output, sourceChecks, destChecks, orphanedRefs, summary)
}

func parseFlags() (*options, error) {
	var opts options
	flag.StringVar(&opts.Output, "output", outputTable, "Output format: table or json")
	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.DBSSLMode, "db-sslmode", "", "Optional database sslmode override, for example: require")
	flag.StringVar(&opts.VerifyCase, "verify-case", caseAll, "Verify specific case: A, B, or all")
	flag.Parse()

	opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
	opts.AreaName = strings.TrimSpace(opts.AreaName)
	opts.KandangLocationName = strings.TrimSpace(opts.KandangLocationName)
	opts.DBSSLMode = strings.TrimSpace(opts.DBSSLMode)
	opts.VerifyCase = strings.ToUpper(strings.TrimSpace(opts.VerifyCase))

	if opts.Output == "" {
		opts.Output = outputTable
	}
	if opts.Output != outputTable && opts.Output != outputJSON {
		return nil, fmt.Errorf("unsupported --output=%s", opts.Output)
	}
	if opts.VerifyCase == "" {
		opts.VerifyCase = caseAll
	}
	if opts.VerifyCase != caseA && opts.VerifyCase != caseB && opts.VerifyCase != caseAll {
		return nil, fmt.Errorf("unsupported --verify-case=%s", opts.VerifyCase)
	}

	return &opts, nil
}

func verifySourceWarehouses(ctx context.Context, db *gorm.DB, opts *options) ([]sourceWarehouseCheck, error) {
	filters, args := buildFilters(opts)
	query := fmt.Sprintf(`
WITH case_a_warehouses AS (
	-- Case A: Kandang-level warehouses (type != 'LOKASI' or NULL)
	SELECT
		w.id,
		a.name AS area_name,
		kl.name AS kandang_location_name,
		k.id,
		k.name,
		'A'::text AS case_type
	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
	WHERE w.deleted_at IS NOT NULL
	  AND w.kandang_id IS NOT NULL
	  AND UPPER(COALESCE(w.type, '')) <> 'LOKASI'
),
case_b_warehouses AS (
	-- Case B: Wrong-location warehouses (location_id != kandang.location_id)
	SELECT
		w.id,
		a.name AS area_name,
		kl.name AS kandang_location_name,
		k.id,
		k.name,
		'B'::text AS case_type
	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
	WHERE w.deleted_at IS NOT NULL
	  AND w.kandang_id IS NOT NULL
	  AND w.location_id IS DISTINCT FROM k.location_id
),
all_source_warehouses AS (
	SELECT w_id, area_name, kandang_location_name, k_id AS kandang_id, name, case_type FROM (
		SELECT w.id as w_id, a.name AS area_name, kl.name AS kandang_location_name, k.id as k_id, k.name, 'A'::text AS case_type
		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
		WHERE w.deleted_at IS NOT NULL
		  AND w.kandang_id IS NOT NULL
		  AND UPPER(COALESCE(w.type, '')) <> 'LOKASI'
	) case_a_warehouses
	UNION ALL
	SELECT w_id, area_name, kandang_location_name, k_id AS kandang_id, name, case_type FROM (
		SELECT w.id as w_id, a.name AS area_name, kl.name AS kandang_location_name, k.id as k_id, k.name, 'B'::text AS case_type
		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
		WHERE w.deleted_at IS NOT NULL
		  AND w.kandang_id IS NOT NULL
		  AND w.location_id IS DISTINCT FROM k.location_id
	) case_b_warehouses
)
SELECT
	asw.area_name,
	asw.kandang_location_name,
	asw.kandang_id,
	asw.name AS kandang_name,
	w.id AS source_warehouse_id,
	w.name AS source_warehouse_name,
	asw.case_type,
	TO_CHAR(w.deleted_at, 'YYYY-MM-DD') AS deleted_at,
	COALESCE(SUM(pw.qty), 0) AS stock_in_product_wh,
	COUNT(DISTINCT pi.id) AS active_purchase_items
FROM all_source_warehouses asw
JOIN warehouses w ON w.id = asw.w_id
LEFT JOIN product_warehouses pw ON pw.warehouse_id = w.id
LEFT JOIN purchase_items pi ON pi.warehouse_id = w.id
WHERE true
  %s
GROUP BY
	asw.area_name,
	asw.kandang_location_name,
	asw.kandang_id,
	asw.name,
	w.id,
	w.name,
	asw.case_type,
	w.deleted_at
ORDER BY asw.area_name ASC, asw.kandang_location_name ASC, w.name ASC
`, andClause(filters))

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

	// Determine status for each row
	for i := range rows {
		if rows[i].StockInProductWH == 0 && rows[i].ActivePurchaseItems == 0 {
			rows[i].Status = "CLEAN"
		} else {
			rows[i].Status = "DIRTY"
		}

		// Filter by case if requested
		if opts.VerifyCase != caseAll && rows[i].Case != opts.VerifyCase {
			rows = append(rows[:i], rows[i+1:]...)
			i--
		}
	}

	return rows, nil
}

func verifyDestinationWarehouses(ctx context.Context, db *gorm.DB, opts *options, sourceChecks []sourceWarehouseCheck) ([]destinationWarehouseCheck, error) {
	filters, args := buildFilters(opts)
	query := fmt.Sprintf(`
SELECT
	a.name AS area_name,
	kl.name AS kandang_location_name,
	fw.id AS farm_warehouse_id,
	fw.name AS farm_warehouse_name,
	p.id AS product_id,
	p.name AS product_name,
	COALESCE(pw.qty, 0) AS current_qty,
	COALESCE(SUM(sl.stock), 0) AS stock_logs_total,
	COUNT(DISTINCT sl.id) AS stock_logs_count
FROM warehouses fw
JOIN locations kl ON kl.id = fw.location_id
JOIN areas a ON a.id = kl.area_id
JOIN product_warehouses pw ON pw.warehouse_id = fw.id
JOIN products p ON p.id = pw.product_id
JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products' AND UPPER(f.name) IN ('PAKAN', 'OVK')
LEFT JOIN stock_logs sl ON sl.product_warehouse_id = pw.id
WHERE fw.deleted_at IS NULL
  AND UPPER(COALESCE(fw.type, '')) = 'LOKASI'
  %s
GROUP BY
	a.name,
	kl.name,
	fw.id,
	fw.name,
	p.id,
	p.name,
	pw.qty
ORDER BY a.name ASC, kl.name ASC, fw.name ASC, p.name ASC
`, andClause(filters))

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

	// Determine status: check if current_qty matches stock_logs
	for i := range rows {
		if rows[i].CurrentQty > 0 {
			// Allow small floating point discrepancies
			if abs(rows[i].CurrentQty-rows[i].StockLogsTotal) < 0.001 {
				rows[i].Status = "MATCHED"
			} else {
				rows[i].Status = "DISCREPANCY"
			}
		} else {
			rows[i].Status = "EMPTY"
		}
	}

	return rows, nil
}

func verifyOrphanedReferences(ctx context.Context, db *gorm.DB, sourceChecks []sourceWarehouseCheck) ([]orphanedReferenceCheck, error) {
	if len(sourceChecks) == 0 {
		return []orphanedReferenceCheck{}, nil
	}

	// Get unique warehouse IDs from source checks
	warehouseIDs := make([]uint, 0)
	for _, check := range sourceChecks {
		warehouseIDs = append(warehouseIDs, check.SourceWarehouseID)
	}

	// Check common references
	var results []orphanedReferenceCheck

	refChecks := []struct {
		table  string
		column string
	}{
		{"purchase_items", "warehouse_id"},
		{"stock_transfers", "from_warehouse_id"},
		{"stock_transfers", "to_warehouse_id"},
	}

	for _, ref := range refChecks {
		var count int64
		if err := db.Table(ref.table).
			Where(fmt.Sprintf("%s IN ?", ref.column), warehouseIDs).
			Count(&count).Error; err != nil {
			return nil, err
		}

		if count > 0 {
			// Get the specific warehouse IDs using raw SQL
			var ids []uint
			query := fmt.Sprintf("SELECT DISTINCT %s FROM %s WHERE %s IN ?",
				ref.column, ref.table, ref.column)
			if err := db.Raw(query, warehouseIDs).Scan(&ids).Error; err != nil {
				return nil, err
			}

			idStrs := make([]string, 0, len(ids))
			for _, id := range ids {
				idStrs = append(idStrs, fmt.Sprintf("%d", id))
			}

			results = append(results, orphanedReferenceCheck{
				Table:               ref.table,
				Column:              ref.column,
				ReferenceCount:      count,
				DeletedWarehouseIDs: strings.Join(idStrs, ", "),
			})
		}
	}

	return results, nil
}

func buildFilters(opts *options) ([]string, []any) {
	filters := make([]string, 0, 2)
	args := make([]any, 0, 2)
	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)
	}
	return filters, args
}

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

func buildSummary(sourceChecks []sourceWarehouseCheck, destChecks []destinationWarehouseCheck, orphanedRefs []orphanedReferenceCheck) verificationSummary {
	summary := verificationSummary{
		TotalSourceWarehouses: len(sourceChecks),
		OverallStatus:         "PASS",
	}

	for _, check := range sourceChecks {
		if check.Status == "CLEAN" {
			summary.CleanSourceWarehouses++
		} else {
			summary.DirtySourceWarehouses++
			summary.OverallStatus = "FAIL"
		}
	}

	summary.TotalDestinationWarehouses = len(destChecks)
	for _, check := range destChecks {
		if check.Status == "MATCHED" || check.Status == "EMPTY" {
			summary.MatchingDestinations++
		} else if check.Status == "DISCREPANCY" {
			summary.DiscrepancyDestinations++
			summary.OverallStatus = "FAIL"
		}
	}

	for _, ref := range orphanedRefs {
		summary.TotalOrphanedReferences += ref.ReferenceCount
		summary.OverallStatus = "FAIL"
	}

	return summary
}

func renderVerification(mode string, sourceChecks []sourceWarehouseCheck, destChecks []destinationWarehouseCheck, orphanedRefs []orphanedReferenceCheck, summary verificationSummary) {
	if mode == outputJSON {
		payload := map[string]any{
			"source_warehouses":      sourceChecks,
			"destination_warehouses": destChecks,
			"orphaned_references":    orphanedRefs,
			"summary":                summary,
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		_ = enc.Encode(payload)
		return
	}

	// Table mode
	fmt.Println("\n=== SOURCE WAREHOUSES VERIFICATION ===")
	if len(sourceChecks) == 0 {
		fmt.Println("No deleted warehouses found")
	} else {
		w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
		fmt.Fprintln(w, "AREA\tLOKASI\tKANDANG\tWAREHOUSE\tCASE\tDELETED_AT\tSTOCK_IN_PW\tPURCHASE_ITEMS\tSTATUS")
		for _, check := range sourceChecks {
			fmt.Fprintf(
				w,
				"%s\t%s\t%s\t%s\t%s\t%s\t%.3f\t%d\t%s\n",
				check.AreaName,
				check.KandangLocationName,
				check.KandangName,
				check.SourceWarehouseName,
				check.Case,
				displayOptionalString(check.DeletedAt),
				check.StockInProductWH,
				check.ActivePurchaseItems,
				check.Status,
			)
		}
		_ = w.Flush()
	}

	fmt.Println("\n=== DESTINATION WAREHOUSES VERIFICATION ===")
	if len(destChecks) == 0 {
		fmt.Println("No destination warehouses found")
	} else {
		w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
		fmt.Fprintln(w, "AREA\tLOKASI\tFARM_WAREHOUSE\tPRODUCT\tCURRENT_QTY\tSTOCK_LOGS_TOTAL\tLOGS_COUNT\tSTATUS")
		for _, check := range destChecks {
			fmt.Fprintf(
				w,
				"%s\t%s\t%s\t%s\t%.3f\t%.3f\t%d\t%s\n",
				check.AreaName,
				check.KandangLocationName,
				check.FarmWarehouseName,
				check.ProductName,
				check.CurrentQty,
				check.StockLogsTotal,
				check.StockLogsCount,
				check.Status,
			)
		}
		_ = w.Flush()
	}

	if len(orphanedRefs) > 0 {
		fmt.Println("\n=== ORPHANED REFERENCES (ERRORS) ===")
		w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
		fmt.Fprintln(w, "TABLE\tCOLUMN\tCOUNT\tWAREHOUSE_IDS")
		for _, ref := range orphanedRefs {
			fmt.Fprintf(
				w,
				"%s\t%s\t%d\t%s\n",
				ref.Table,
				ref.Column,
				ref.ReferenceCount,
				ref.DeletedWarehouseIDs,
			)
		}
		_ = w.Flush()
	}

	fmt.Printf("\n=== SUMMARY ===\n")
	fmt.Printf("Source Warehouses: %d total, %d clean, %d dirty\n", summary.TotalSourceWarehouses, summary.CleanSourceWarehouses, summary.DirtySourceWarehouses)
	fmt.Printf("Destination Warehouses: %d total, %d matching, %d discrepancies\n", summary.TotalDestinationWarehouses, summary.MatchingDestinations, summary.DiscrepancyDestinations)
	fmt.Printf("Orphaned References: %d\n", summary.TotalOrphanedReferences)
	fmt.Printf("Overall Status: %s\n", summary.OverallStatus)
}

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

func abs(x float64) float64 {
	if x < 0 {
		return -x
	}
	return x
}
