package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"gopkg.in/yaml.v2"
)

type Cfg struct {
	Settings      Settings      `yaml:"Settings"`
	DBConfig      DBConfig      `yaml:"DBConfig"`
	FormatColumns FormatColumns `yaml:"FormatColumns"`
}

type Settings struct {
	Condition    string `yaml:"Condition"`
	PrimaryKey   string `yaml:"PrimaryKey"`
	DeleteSource bool   `yaml:"DeleteSource"`
}

type FormatColumns struct {
	Enablement bool   `yaml:"Enablement"`
	Reference  string `yaml:"Reference"`
	Year       string `yaml:"Year"`
	Month      string `yaml:"Month"`
	Day        string `yaml:"Day"`
	Week       string `yaml:"Week"`
}

type DBConfig struct {
	Username           string   `yaml:"Username"`
	Password           string   `yaml:"Password"`
	Server             string   `yaml:"Server"`
	Port               string   `yaml:"Port"`
	Database           string   `yaml:"Database"`
	SourceTable        string   `yaml:"SourceTable"`
	DestinationTable   string   `yaml:"DestinationTable"`
	SourceColumns      []string `yaml:"SourceColumns"`
	DestinationColumns []string `yaml:"DestinationColumns"`
}

var cfg = Cfg{}
var dsn string

func main() {
	log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
	log.SetOutput(os.Stdout)

	self := strings.TrimSuffix(os.Args[0], ".exe")
	file := self + ".yml"
	txt, err := os.ReadFile(file)
	if err != nil {
		log.Printf("[ERR]  %s\r\n", err.Error())
		os.Exit(1)
	}
	err = yaml.Unmarshal(txt, &cfg)
	if err != nil {
		log.Printf("[ERR]  %s\r\n", err.Error())
		os.Exit(1)
	}

	// srcCols := cfg.DBConfig.Columns
	// dstCols := cfg.DBConfig.Columns
	// if len(srcCols) != len(dstCols) {
	// 	log.Println("[ERR]  Mismatched Length Of Source Columns And Destination Columns.")
	// 	os.Exit(1)
	// }

	condition := cfg.Settings.Condition
	log.Printf("[MSG]  Condition: %s\r\n", condition)

	if condition == "" {
		log.Println("[ERR]  Missing Query Condition.")
		os.Exit(1)
	}

	dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s",
		cfg.DBConfig.Username,
		cfg.DBConfig.Password,
		cfg.DBConfig.Server,
		cfg.DBConfig.Port,
		cfg.DBConfig.Database,
	)

	qry, err := retrieveData(condition)
	if err != nil {
		log.Printf("[ERR]  %v\r\n", err)
		os.Exit(1)
	}
	log.Printf("[MSG]  Selected Rows: %d\r\n", len(qry))
	if len(qry) == 0 {
		os.Exit(0)
	}

	err = backupData(qry)
	if err != nil {
		log.Printf("[ERR]  %v\r\n", err)
		os.Exit(1)
	}
}

func retrieveData(condition string) ([]map[string]interface{}, error) {
	rst := make([]map[string]interface{}, 0)
	dbo, err := sql.Open("mysql", dsn)
	if err != nil {
		return rst, err
	}
	defer dbo.Close()
	err = dbo.Ping()
	if err != nil {
		return rst, err
	}

	cols := ""
	for _, c := range cfg.DBConfig.SourceColumns {
		cols = cols + c + ","
	}
	cols = strings.TrimSuffix(cols, ",")

	qry := fmt.Sprintf("SELECT %s FROM %s.%s",
		cols,
		cfg.DBConfig.Database,
		cfg.DBConfig.SourceTable,
	)
	flt := fmt.Sprintf("WHERE %s;", condition)
	if condition != "" {
		qry = qry + " " + flt
	}

	lnth := len(cfg.DBConfig.SourceColumns)
	vals := make([]interface{}, lnth)
	valsPtr := make([]interface{}, lnth)
	for i := 0; i < lnth; i++ {
		valsPtr[i] = &vals[i]
	}

	rows, err := dbo.Query(qry)
	if err != nil {
		return rst, err
	}
	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(valsPtr...)
		if err != nil {
			break
		}
		row := make(map[string]interface{}, 0)
		for i, val := range vals {
			key := cfg.DBConfig.SourceColumns[i]
			var v interface{}
			b, ok := val.([]byte)
			if ok {
				v = string(b)
			} else {
				v = val
			}
			row[key] = v
		}
		rst = append(rst, row)
	}
	return rst, nil
}

func backupData(data []map[string]interface{}) error {
	dbo, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	defer dbo.Close()
	err = dbo.Ping()
	if err != nil {
		return err
	}

	cols := ""
	for _, c := range cfg.DBConfig.DestinationColumns {
		cols = cols + c + ","
	}

	// Append Formated Column Keys
	if cfg.FormatColumns.Enablement {
		cols = cols + strings.Join(
			[]string{
				cfg.FormatColumns.Year,
				cfg.FormatColumns.Month,
				cfg.FormatColumns.Day,
				cfg.FormatColumns.Week,
			},
			",",
		)
	}
	cols = strings.TrimSuffix(cols, ",")

	var timeStr string = ""
	var timeVal time.Time
	for i, d := range data {
		log.Printf("[MSG]  #%d:  %#v\r\n", i+1, d)
		vals := ""
		for _, c := range cfg.DBConfig.DestinationColumns {
			for k, v := range d {
				if k == c {
					vals = vals + fmt.Sprintf("%#v,", v)
				}
				if k == cfg.FormatColumns.Reference {
					timeStr = v.(string)
					timeVal, err = time.Parse("2006-01-02 15:04:05", timeStr)
					if err != nil {
						log.Printf("[ERR]  #%d:  %#v\r\n", i+1, err)
						continue
					}
				}
			}
		}

		// Append Formated Column Values
		if cfg.FormatColumns.Enablement {
			vals = vals + strings.Join(
				[]string{
					fmt.Sprintf("%#v", timeVal.Format("2006")),
					fmt.Sprintf("%#v", timeVal.Format("01")),
					fmt.Sprintf("%#v", timeVal.Format("02")),
					fmt.Sprintf("%#v", calculateWeek(timeVal)),
				},
				",",
			)
		}
		vals = strings.TrimSuffix(vals, ",")

		tx, err := dbo.Begin()
		if err != nil {
			tx.Rollback()
			log.Printf("[ERR]  txBegin: %v\r\n\r\n", err)
			continue
		}

		stmt, err := tx.Prepare(fmt.Sprintf("INSERT INTO %s.%s (%s) VALUES (%s);",
			cfg.DBConfig.Database,
			cfg.DBConfig.DestinationTable,
			cols,
			vals,
		))
		if err != nil {
			tx.Rollback()
			log.Printf("[ERR]  txPrepare_Insert: %v\r\n\r\n", err)
			continue
		}
		defer stmt.Close()

		_, err = stmt.Exec()
		if err != nil {
			tx.Rollback()
			log.Printf("[ERR]  txExecute_Insert: %v\r\n\r\n", err)
			continue
		}
		log.Printf("[MSG]  Backed Up: %s=%s\r\n", cfg.Settings.PrimaryKey, d[cfg.Settings.PrimaryKey])

		if cfg.Settings.DeleteSource {
			stmt, err = tx.Prepare(fmt.Sprintf("DELETE FROM %s.%s WHERE %s=%s;",
				cfg.DBConfig.Database,
				cfg.DBConfig.SourceTable,
				cfg.Settings.PrimaryKey,
				fmt.Sprintf("%#v", d[cfg.Settings.PrimaryKey]),
			))
			if err != nil {
				tx.Rollback()
				log.Printf("[ERR]  txPrepare_Delete: %v\r\n\r\n", err)
				continue
			}
			defer stmt.Close()

			_, err = stmt.Exec()
			if err != nil {
				tx.Rollback()
				log.Printf("[ERR]  txExecute_Delete: %v\r\n\r\n", err)
				continue
			}
			log.Printf("[MSG]  Deleted Souce: %s=%s\r\n", cfg.Settings.PrimaryKey, d[cfg.Settings.PrimaryKey])
		}

		err = tx.Commit()
		if err != nil {
			tx.Rollback()
			log.Printf("[ERR]  txCommit: %v\r\n\r\n", err)
			continue
		}
		log.Printf("[MSG]  **********  Transaction Commited.\r\n\r\n")
		time.Sleep(time.Millisecond * 100)
	}
	return nil
}

func calculateWeek(t time.Time) string {
	yearDay := t.YearDay()
	lastYearEndDay := t.AddDate(0, 0, -yearDay)
	lastYearEndDayWeek := int(lastYearEndDay.Weekday())
	firstWeekDays := 7
	if lastYearEndDayWeek != 0 {
		firstWeekDays = 7 - lastYearEndDayWeek
	}
	week := 0
	if yearDay <= firstWeekDays {
		week = 1
	} else {
		plusDay := 0
		if (yearDay-firstWeekDays)%7 > 0 {
			plusDay = 1
		}
		week = (yearDay-firstWeekDays)/7 + 1 + plusDay
	}
	return fmt.Sprintf("%02d", week)
}
