package identity

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io/fs"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/xuri/excelize/v2"
)

const (
	IdentifierTypeJT808Phone = "JT808_PHONE"
	IdentifierTypePlate      = "PLATE"
)

type MappingRecord struct {
	File            string `json:"file"`
	Sheet           string `json:"sheet"`
	Row             int    `json:"row"`
	SourceCode      string `json:"source_code"`
	SourceName      string `json:"source_name"`
	Protocol        string `json:"protocol"`
	IdentifierType  string `json:"identifier_type"`
	IdentifierValue string `json:"identifier_value"`
	RawValue        string `json:"raw_value,omitempty"`
	Plate           string `json:"plate,omitempty"`
	OEM             string `json:"oem,omitempty"`
}

type MappingScanReport struct {
	Files            int                            `json:"files"`
	Sheets           int                            `json:"sheets"`
	Rows             int                            `json:"rows"`
	Records          int                            `json:"records"`
	Skipped          int                            `json:"skipped"`
	UnsupportedFiles int                            `json:"unsupported_files,omitempty"`
	Sources          []MappingSourceScanReport      `json:"sources,omitempty"`
	UnsupportedItems []MappingUnsupportedFileReport `json:"unsupported_items,omitempty"`
	Errors           []string                       `json:"errors,omitempty"`
}

type MappingSourceScanReport struct {
	SourceCode   string `json:"source_code"`
	SourceName   string `json:"source_name"`
	Files        int    `json:"files"`
	Sheets       int    `json:"sheets"`
	Rows         int    `json:"rows"`
	Records      int    `json:"records"`
	PhoneRecords int    `json:"phone_records"`
	PlateRecords int    `json:"plate_records"`
	Skipped      int    `json:"skipped"`
}

type MappingUnsupportedFileReport struct {
	File   string `json:"file"`
	Ext    string `json:"ext"`
	Reason string `json:"reason"`
}

type MappingImportOptions struct {
	Apply           bool
	LegacyTable     string
	ReportItemLimit int
}

type MappingImportReport struct {
	Scan            MappingScanReport         `json:"scan"`
	Records         int                       `json:"records"`
	Deduplicated    int                       `json:"deduplicated"`
	Resolved        int                       `json:"resolved"`
	Unresolved      int                       `json:"unresolved"`
	Conflicts       int                       `json:"conflicts"`
	WouldInsert     int                       `json:"would_insert,omitempty"`
	WouldUpdate     int                       `json:"would_update,omitempty"`
	Inserted        int                       `json:"inserted,omitempty"`
	Updated         int                       `json:"updated,omitempty"`
	SourceResults   []MappingSourceImportStat `json:"source_results,omitempty"`
	UnresolvedItems []MappingRecord           `json:"unresolved_items,omitempty"`
	ConflictItems   []MappingConflict         `json:"conflict_items,omitempty"`
}

type MappingSourceImportStat struct {
	SourceCode   string `json:"source_code"`
	SourceName   string `json:"source_name"`
	Records      int    `json:"records"`
	Deduplicated int    `json:"deduplicated"`
	Resolved     int    `json:"resolved"`
	Unresolved   int    `json:"unresolved"`
	Conflicts    int    `json:"conflicts"`
	WouldInsert  int    `json:"would_insert,omitempty"`
	WouldUpdate  int    `json:"would_update,omitempty"`
	Inserted     int    `json:"inserted,omitempty"`
	Updated      int    `json:"updated,omitempty"`
}

type MappingConflict struct {
	Record      MappingRecord `json:"record"`
	ExistingVIN string        `json:"existing_vin,omitempty"`
	NewVIN      string        `json:"new_vin,omitempty"`
	Reason      string        `json:"reason"`
}

type resolvedMappingRecord struct {
	MappingRecord
	VIN string
}

type mappingStore interface {
	ExecContext(context.Context, string, ...any) (sql.Result, error)
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

var digitPattern = regexp.MustCompile(`\D+`)

func EnsureVehicleIdentifierSchema(ctx context.Context, db *sql.DB) error {
	if db == nil {
		return errors.New("identity db must not be nil")
	}
	if _, err := db.ExecContext(ctx, vehicleTableSQL); err != nil {
		return err
	}
	_, err := db.ExecContext(ctx, vehicleIdentifierTableSQL)
	return err
}

func ReadMappingDirectory(root string) ([]MappingRecord, MappingScanReport, error) {
	root = strings.TrimSpace(root)
	if root == "" {
		return nil, MappingScanReport{}, errors.New("mapping input directory is empty")
	}
	var records []MappingRecord
	report := MappingScanReport{}
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			report.Errors = append(report.Errors, err.Error())
			return nil
		}
		if entry == nil || entry.IsDir() {
			return nil
		}
		name := entry.Name()
		if strings.HasPrefix(name, "~$") || strings.HasPrefix(name, "._") {
			return nil
		}
		ext := strings.ToLower(filepath.Ext(name))
		if !isSupportedMappingWorkbookExt(ext) {
			if isUnsupportedMappingWorkbookExt(ext) {
				report.UnsupportedFiles++
				report.UnsupportedItems = append(report.UnsupportedItems, MappingUnsupportedFileReport{
					File:   path,
					Ext:    ext,
					Reason: "convert legacy workbook to .xlsx before import",
				})
			}
			return nil
		}
		fileRecords, fileReport, err := readMappingWorkbook(root, path)
		report.Files++
		report.Sheets += fileReport.Sheets
		report.Rows += fileReport.Rows
		report.Records += fileReport.Records
		report.Skipped += fileReport.Skipped
		report.Errors = append(report.Errors, fileReport.Errors...)
		for _, source := range fileReport.Sources {
			mergeMappingSourceScan(&report, source)
		}
		if err != nil {
			report.Errors = append(report.Errors, fmt.Sprintf("%s: %v", path, err))
			return nil
		}
		records = append(records, fileRecords...)
		return nil
	})
	if err != nil {
		return records, report, err
	}
	sortMappingSourceScans(report.Sources)
	return records, report, nil
}

func readMappingWorkbook(root string, path string) ([]MappingRecord, MappingScanReport, error) {
	workbook, err := excelize.OpenFile(path)
	if err != nil {
		return nil, MappingScanReport{}, err
	}
	defer func() { _ = workbook.Close() }()

	sourceCode, sourceName := mappingSource(root, path)
	sourceReport := MappingSourceScanReport{
		SourceCode: sourceCode,
		SourceName: sourceName,
		Files:      1,
	}
	var records []MappingRecord
	report := MappingScanReport{}
	for _, sheet := range workbook.GetSheetList() {
		rows, err := workbook.GetRows(sheet)
		if err != nil {
			report.Errors = append(report.Errors, fmt.Sprintf("%s/%s: %v", path, sheet, err))
			continue
		}
		report.Sheets++
		sourceReport.Sheets++
		report.Rows += len(rows)
		sourceReport.Rows += len(rows)
		if len(rows) == 0 {
			continue
		}
		header, dataStart := mappingHeader(rows)
		for rowIndex := dataStart; rowIndex < len(rows); rowIndex++ {
			row := rows[rowIndex]
			plate := normalizePlate(cellByHeader(row, header, "plate"))
			rawPhone := cellByHeader(row, header, "phone")
			phone := normalizeMappingPhone(rawPhone)
			if len(header) == 0 {
				plate = normalizePlate(cell(row, 0))
				rawPhone = cell(row, 1)
				phone = normalizeMappingPhone(rawPhone)
			}
			if plate == "" && phone == "" {
				report.Skipped++
				sourceReport.Skipped++
				continue
			}
			if phone != "" {
				records = append(records, MappingRecord{
					File:            path,
					Sheet:           sheet,
					Row:             rowIndex + 1,
					SourceCode:      sourceCode,
					SourceName:      sourceName,
					Protocol:        "JT808",
					IdentifierType:  IdentifierTypeJT808Phone,
					IdentifierValue: phone,
					RawValue:        strings.TrimSpace(rawPhone),
					Plate:           plate,
					OEM:             sourceName,
				})
				sourceReport.PhoneRecords++
				sourceReport.Records++
			}
			if plate != "" {
				records = append(records, MappingRecord{
					File:            path,
					Sheet:           sheet,
					Row:             rowIndex + 1,
					SourceCode:      sourceCode,
					SourceName:      sourceName,
					Protocol:        "JT808",
					IdentifierType:  IdentifierTypePlate,
					IdentifierValue: plate,
					RawValue:        plate,
					Plate:           plate,
					OEM:             sourceName,
				})
				sourceReport.PlateRecords++
				sourceReport.Records++
			}
		}
	}
	report.Records = len(records)
	report.Sources = []MappingSourceScanReport{sourceReport}
	return records, report, nil
}

func mergeMappingSourceScan(report *MappingScanReport, source MappingSourceScanReport) {
	if report == nil || strings.TrimSpace(source.SourceCode) == "" {
		return
	}
	for index := range report.Sources {
		if report.Sources[index].SourceCode != source.SourceCode {
			continue
		}
		report.Sources[index].Files += source.Files
		report.Sources[index].Sheets += source.Sheets
		report.Sources[index].Rows += source.Rows
		report.Sources[index].Records += source.Records
		report.Sources[index].PhoneRecords += source.PhoneRecords
		report.Sources[index].PlateRecords += source.PlateRecords
		report.Sources[index].Skipped += source.Skipped
		if report.Sources[index].SourceName == "" {
			report.Sources[index].SourceName = source.SourceName
		}
		return
	}
	report.Sources = append(report.Sources, source)
}

func sortMappingSourceScans(sources []MappingSourceScanReport) {
	sort.SliceStable(sources, func(i, j int) bool {
		return sources[i].SourceCode < sources[j].SourceCode
	})
}

func isSupportedMappingWorkbookExt(ext string) bool {
	switch strings.ToLower(strings.TrimSpace(ext)) {
	case ".xlsx", ".xlsm":
		return true
	default:
		return false
	}
}

func isUnsupportedMappingWorkbookExt(ext string) bool {
	switch strings.ToLower(strings.TrimSpace(ext)) {
	case ".xls", ".xlsb":
		return true
	default:
		return false
	}
}

func ImportMappingRecords(ctx context.Context, db *sql.DB, records []MappingRecord, scan MappingScanReport, opts MappingImportOptions) (MappingImportReport, error) {
	if db == nil {
		return MappingImportReport{}, errors.New("identity db must not be nil")
	}
	legacyTable := strings.TrimSpace(opts.LegacyTable)
	if legacyTable == "" || !safeIdentifier(legacyTable) {
		legacyTable = "vehicle_identity_binding"
	}
	report := MappingImportReport{
		Scan:    scan,
		Records: len(records),
	}
	sourceStats := map[string]*MappingSourceImportStat{}
	for _, record := range records {
		sourceImportStat(sourceStats, record).Records++
	}
	reportLimit := opts.ReportItemLimit
	if reportLimit == 0 {
		reportLimit = 50
	}
	deduped, conflicts := dedupeMappingRecords(records)
	report.Deduplicated = len(deduped)
	report.Conflicts += len(conflicts)
	for _, conflict := range conflicts {
		sourceImportStat(sourceStats, conflict.Record).Conflicts++
		appendConflictItem(&report, conflict, reportLimit)
	}

	store := mappingStore(db)
	var tx *sql.Tx
	if opts.Apply {
		var err error
		tx, err = db.BeginTx(ctx, nil)
		if err != nil {
			return report, err
		}
		store = tx
		defer func() {
			if tx != nil {
				_ = tx.Rollback()
			}
		}()
	}

	for _, record := range deduped {
		sourceStat := sourceImportStat(sourceStats, record)
		sourceStat.Deduplicated++
		vin, err := resolveMappingVIN(ctx, store, legacyTable, record)
		if err != nil {
			return report, err
		}
		if vin == "" {
			report.Unresolved++
			sourceStat.Unresolved++
			appendUnresolvedItem(&report, record, reportLimit)
			continue
		}
		resolved := resolvedMappingRecord{MappingRecord: record, VIN: vin}
		existingVIN, exists, err := existingIdentifierVIN(ctx, store, resolved)
		if err != nil {
			return report, err
		}
		if exists && !strings.EqualFold(existingVIN, vin) {
			report.Conflicts++
			sourceStat.Conflicts++
			appendConflictItem(&report, MappingConflict{
				Record:      record,
				ExistingVIN: existingVIN,
				NewVIN:      vin,
				Reason:      "identifier already points to another vin",
			}, reportLimit)
			continue
		}
		report.Resolved++
		sourceStat.Resolved++
		if exists {
			if opts.Apply {
				if err := updateVehicleIdentifier(ctx, store, resolved); err != nil {
					return report, err
				}
				report.Updated++
				sourceStat.Updated++
			} else {
				report.WouldUpdate++
				sourceStat.WouldUpdate++
			}
			continue
		}
		if opts.Apply {
			if err := upsertVehicle(ctx, store, resolved); err != nil {
				return report, err
			}
			if err := insertVehicleIdentifier(ctx, store, resolved); err != nil {
				return report, err
			}
			report.Inserted++
			sourceStat.Inserted++
		} else {
			report.WouldInsert++
			sourceStat.WouldInsert++
		}
	}
	if tx != nil {
		if err := tx.Commit(); err != nil {
			return report, err
		}
		tx = nil
	}
	report.SourceResults = sortedMappingSourceImportStats(sourceStats)
	return report, nil
}

func sourceImportStat(stats map[string]*MappingSourceImportStat, record MappingRecord) *MappingSourceImportStat {
	sourceCode := strings.TrimSpace(record.SourceCode)
	if sourceCode == "" {
		sourceCode = "unknown"
	}
	stat := stats[sourceCode]
	if stat != nil {
		if stat.SourceName == "" {
			stat.SourceName = strings.TrimSpace(record.SourceName)
		}
		return stat
	}
	stat = &MappingSourceImportStat{
		SourceCode: sourceCode,
		SourceName: strings.TrimSpace(record.SourceName),
	}
	stats[sourceCode] = stat
	return stat
}

func sortedMappingSourceImportStats(stats map[string]*MappingSourceImportStat) []MappingSourceImportStat {
	if len(stats) == 0 {
		return nil
	}
	keys := make([]string, 0, len(stats))
	for key := range stats {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	out := make([]MappingSourceImportStat, 0, len(keys))
	for _, key := range keys {
		out = append(out, *stats[key])
	}
	return out
}

func appendUnresolvedItem(report *MappingImportReport, record MappingRecord, limit int) {
	if report == nil {
		return
	}
	if limit >= 0 && len(report.UnresolvedItems) >= limit {
		return
	}
	report.UnresolvedItems = append(report.UnresolvedItems, record)
}

func appendConflictItem(report *MappingImportReport, conflict MappingConflict, limit int) {
	if report == nil {
		return
	}
	if limit >= 0 && len(report.ConflictItems) >= limit {
		return
	}
	report.ConflictItems = append(report.ConflictItems, conflict)
}

func dedupeMappingRecords(records []MappingRecord) ([]MappingRecord, []MappingConflict) {
	seen := map[string]MappingRecord{}
	indexByKey := map[string]int{}
	var out []MappingRecord
	var conflicts []MappingConflict
	for _, record := range records {
		record.IdentifierValue = normalizeIdentifierValue(record.IdentifierType, record.IdentifierValue)
		record.Plate = normalizePlate(record.Plate)
		if record.Protocol == "" {
			record.Protocol = "JT808"
		}
		if record.IdentifierValue == "" || record.IdentifierType == "" {
			continue
		}
		key := strings.Join([]string{record.Protocol, record.SourceCode, record.IdentifierType, record.IdentifierValue}, "\x00")
		existing, ok := seen[key]
		if !ok {
			seen[key] = record
			indexByKey[key] = len(out)
			out = append(out, record)
			continue
		}
		if existing.Plate != "" && record.Plate != "" && existing.Plate != record.Plate {
			conflicts = append(conflicts, MappingConflict{
				Record: record,
				Reason: fmt.Sprintf("same source identifier maps to multiple plates: %s/%s", existing.Plate, record.Plate),
			})
			continue
		}
		merged := mergeMappingRecord(existing, record)
		seen[key] = merged
		if index, ok := indexByKey[key]; ok && index >= 0 && index < len(out) {
			out[index] = merged
		}
	}
	return out, conflicts
}

func mergeMappingRecord(existing MappingRecord, incoming MappingRecord) MappingRecord {
	merged := existing
	if merged.File == "" {
		merged.File = incoming.File
	}
	if merged.Sheet == "" {
		merged.Sheet = incoming.Sheet
	}
	if merged.Row == 0 {
		merged.Row = incoming.Row
	}
	if merged.SourceName == "" {
		merged.SourceName = incoming.SourceName
	}
	if merged.Protocol == "" {
		merged.Protocol = incoming.Protocol
	}
	if merged.IdentifierType == "" {
		merged.IdentifierType = incoming.IdentifierType
	}
	if merged.IdentifierValue == "" {
		merged.IdentifierValue = incoming.IdentifierValue
	}
	if merged.RawValue == "" {
		merged.RawValue = incoming.RawValue
	}
	if merged.Plate == "" {
		merged.Plate = incoming.Plate
	}
	if merged.OEM == "" {
		merged.OEM = incoming.OEM
	}
	return merged
}

func resolveMappingVIN(ctx context.Context, db mappingStore, legacyTable string, record MappingRecord) (string, error) {
	if record.Plate != "" {
		vin, err := lookupLegacyVIN(ctx, db, legacyTable, "plate", record.Plate)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			return "", err
		}
		if vin != "" {
			return vin, nil
		}
	}
	if record.IdentifierType == IdentifierTypeJT808Phone && record.IdentifierValue != "" {
		vin, err := lookupLegacyVIN(ctx, db, legacyTable, "phone", record.IdentifierValue)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			return "", err
		}
		return vin, nil
	}
	return "", nil
}

func lookupLegacyVIN(ctx context.Context, db mappingStore, table string, column string, value string) (string, error) {
	if !safeIdentifier(table) || !safeIdentifier(column) {
		return "", sql.ErrNoRows
	}
	query := "SELECT vin FROM " + table + " WHERE " + column + " = ? AND vin IS NOT NULL AND vin <> '' LIMIT 1"
	var vin string
	err := db.QueryRowContext(ctx, query, value).Scan(&vin)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(vin), nil
}

func existingIdentifierVIN(ctx context.Context, db mappingStore, record resolvedMappingRecord) (string, bool, error) {
	var vin string
	err := db.QueryRowContext(ctx, `SELECT vin FROM vehicle_identifier
WHERE protocol = ? AND source_code = ? AND identifier_type = ? AND identifier_value = ?`,
		record.Protocol, record.SourceCode, record.IdentifierType, record.IdentifierValue).Scan(&vin)
	if errors.Is(err, sql.ErrNoRows) {
		return "", false, nil
	}
	if err != nil {
		return "", false, err
	}
	return strings.TrimSpace(vin), true, nil
}

func upsertVehicle(ctx context.Context, db mappingStore, record resolvedMappingRecord) error {
	_, err := db.ExecContext(ctx, `INSERT INTO vehicle (vin, plate, oem, enabled)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE
  plate = IF(VALUES(plate) <> '', VALUES(plate), plate),
  oem = IF(VALUES(oem) <> '', VALUES(oem), oem),
  enabled = 1`,
		record.VIN, record.Plate, record.OEM)
	return err
}

func insertVehicleIdentifier(ctx context.Context, db mappingStore, record resolvedMappingRecord) error {
	_, err := db.ExecContext(ctx, `INSERT INTO vehicle_identifier
  (protocol, source_code, identifier_type, identifier_value, vin, plate, oem, raw_value, enabled, latest_import_file)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
		record.Protocol,
		record.SourceCode,
		record.IdentifierType,
		record.IdentifierValue,
		record.VIN,
		record.Plate,
		record.OEM,
		record.RawValue,
		record.File,
	)
	return err
}

func updateVehicleIdentifier(ctx context.Context, db mappingStore, record resolvedMappingRecord) error {
	_, err := db.ExecContext(ctx, `UPDATE vehicle_identifier
SET plate = IF(? <> '', ?, plate),
    oem = IF(? <> '', ?, oem),
    raw_value = IF(? <> '', ?, raw_value),
    latest_import_file = ?,
    enabled = 1
WHERE protocol = ? AND source_code = ? AND identifier_type = ? AND identifier_value = ?`,
		record.Plate, record.Plate,
		record.OEM, record.OEM,
		record.RawValue, record.RawValue,
		record.File,
		record.Protocol,
		record.SourceCode,
		record.IdentifierType,
		record.IdentifierValue,
	)
	return err
}

func mappingSource(root string, path string) (string, string) {
	rel, err := filepath.Rel(root, path)
	if err != nil {
		rel = filepath.Base(path)
	}
	parts := strings.Split(filepath.ToSlash(rel), "/")
	name := strings.TrimSpace(parts[0])
	if name == "" || strings.EqualFold(name, ".") {
		name = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
	}
	code := sourceCode(name)
	return code, name
}

func sourceCode(name string) string {
	switch strings.TrimSpace(strings.ToLower(name)) {
	case "g7s":
		return "g7s"
	case "信达":
		return "xinda"
	case "广安北斗", "广安车联":
		return "guangan_beidou"
	case "东方北斗":
		return "dongfang_beidou"
	case "赛格":
		return "saige"
	default:
		return normalizeASCIIKey(name)
	}
}

func mappingHeader(rows [][]string) (map[string]int, int) {
	for index, row := range rows {
		header := map[string]int{}
		for columnIndex, value := range row {
			key := normalizeHeader(value)
			switch key {
			case "车牌", "车牌号", "车牌号码":
				header["plate"] = columnIndex
			case "sim", "sim卡号", "手机号", "终端手机号", "终端id", "终端标识":
				header["phone"] = columnIndex
			}
		}
		if len(header) > 0 {
			return header, index + 1
		}
		if index >= 5 {
			break
		}
	}
	return nil, 0
}

func cellByHeader(row []string, header map[string]int, key string) string {
	if len(header) == 0 {
		return ""
	}
	index, ok := header[key]
	if !ok {
		return ""
	}
	return cell(row, index)
}

func cell(row []string, index int) string {
	if index < 0 || index >= len(row) {
		return ""
	}
	return strings.TrimSpace(row[index])
}

func normalizeHeader(value string) string {
	value = strings.TrimSpace(strings.ToLower(value))
	value = strings.ReplaceAll(value, " ", "")
	value = strings.ReplaceAll(value, "\t", "")
	value = strings.ReplaceAll(value, "（", "(")
	value = strings.ReplaceAll(value, "）", ")")
	return value
}

func normalizePlate(value string) string {
	value = strings.TrimSpace(value)
	value = strings.ReplaceAll(value, " ", "")
	value = strings.ReplaceAll(value, "\t", "")
	return value
}

func normalizeMappingPhone(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	if strings.ContainsAny(value, ".eE") {
		if parsed, err := strconv.ParseFloat(value, 64); err == nil && parsed > 0 {
			return normalizePhone(strconv.FormatFloat(parsed, 'f', 0, 64))
		}
	}
	digits := digitPattern.ReplaceAllString(value, "")
	return normalizePhone(digits)
}

func normalizeASCIIKey(value string) string {
	value = strings.TrimSpace(strings.ToLower(value))
	var b strings.Builder
	lastUnderscore := false
	for _, r := range value {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
			lastUnderscore = false
			continue
		}
		if !lastUnderscore {
			b.WriteByte('_')
			lastUnderscore = true
		}
	}
	return strings.Trim(b.String(), "_")
}
