package dbsync

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	_ "github.com/go-sql-driver/mysql"
	_ "github.com/lib/pq"
	_ "modernc.org/sqlite"
)

func Open(driver Driver, dsn string) (*sql.DB, error) {
	var name string
	switch driver {
	case DriverSQLite:
		name = "sqlite"
		if dsn == "" {
			return nil, fmt.Errorf("sqlite dsn required")
		}
		dsn = normalizeSQLiteDSN(dsn)
	case DriverMySQL:
		name = "mysql"
	case DriverPostgres:
		name = "postgres"
	default:
		return nil, fmt.Errorf("unsupported driver: %s", driver)
	}
	db, err := sql.Open(name, dsn)
	if err != nil {
		return nil, err
	}
	if driver == DriverSQLite {
		// SQLite 文件锁：单连接更稳，避免 Windows 上并发 open 卡死/EOF
		db.SetMaxOpenConns(1)
		db.SetMaxIdleConns(1)
	} else {
		db.SetMaxOpenConns(5)
		db.SetMaxIdleConns(2)
	}
	if err := db.Ping(); err != nil {
		_ = db.Close()
		return nil, err
	}
	return db, nil
}

// normalizeSQLiteDSN：Windows 反斜杠 → 正斜杠；裸盘符路径补 file:；补 busy_timeout。
func normalizeSQLiteDSN(dsn string) string {
	dsn = strings.TrimSpace(dsn)
	lower := strings.ToLower(dsn)
	if strings.HasPrefix(lower, "file:") {
		rest := dsn[5:]
		path, query, hasQ := strings.Cut(rest, "?")
		path = strings.ReplaceAll(path, `\`, `/`)
		if hasQ {
			dsn = "file:" + path + "?" + query
		} else {
			dsn = "file:" + path
		}
	} else if strings.Contains(dsn, `\`) || (len(dsn) >= 2 && dsn[1] == ':') {
		path := strings.ReplaceAll(dsn, `\`, `/`)
		dsn = "file:" + path
	}
	if !strings.Contains(strings.ToLower(dsn), "busy_timeout") {
		if strings.Contains(dsn, "?") {
			dsn += "&_pragma=busy_timeout(5000)"
		} else {
			dsn += "?_pragma=busy_timeout(5000)"
		}
	}
	return dsn
}

func quoteIdent(driver Driver, name string) string {
	name = strings.ReplaceAll(name, "`", "")
	name = strings.ReplaceAll(name, `"`, "")
	switch driver {
	case DriverMySQL:
		return "`" + name + "`"
	case DriverPostgres:
		return `"` + name + `"`
	default:
		return `"` + name + `"`
	}
}

func EnsureOutbox(ctx context.Context, db *sql.DB, driver Driver) error {
	var ddl string
	switch driver {
	case DriverSQLite:
		ddl = `CREATE TABLE IF NOT EXISTS ` + OutboxTable + ` (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  table_name TEXT NOT NULL,
  row_pk TEXT NOT NULL,
  op TEXT NOT NULL,
  payload TEXT NOT NULL DEFAULT '',
  version INTEGER NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  synced_at TEXT
);`
	case DriverMySQL:
		ddl = `CREATE TABLE IF NOT EXISTS ` + OutboxTable + ` (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  table_name VARCHAR(128) NOT NULL,
  row_pk VARCHAR(255) NOT NULL,
  op VARCHAR(16) NOT NULL,
  payload LONGTEXT NOT NULL,
  version BIGINT NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  synced_at DATETIME(3) NULL,
  INDEX idx_outbox_unsynced (synced_at, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
	case DriverPostgres:
		ddl = `CREATE TABLE IF NOT EXISTS ` + OutboxTable + ` (
  id BIGSERIAL PRIMARY KEY,
  table_name TEXT NOT NULL,
  row_pk TEXT NOT NULL,
  op TEXT NOT NULL,
  payload TEXT NOT NULL DEFAULT '',
  version BIGINT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  synced_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_ajz_outbox_unsynced ON ` + OutboxTable + ` (synced_at, id);`
	default:
		return fmt.Errorf("unsupported driver")
	}
	_, err := db.ExecContext(ctx, ddl)
	return err
}

func EnsureMeta(ctx context.Context, db *sql.DB, driver Driver) error {
	var ddl string
	switch driver {
	case DriverSQLite:
		ddl = `CREATE TABLE IF NOT EXISTS ` + MetaTable + ` (
  table_name TEXT NOT NULL,
  row_pk TEXT NOT NULL,
  version INTEGER NOT NULL,
  updated_at TEXT NOT NULL,
  PRIMARY KEY (table_name, row_pk)
);`
	case DriverMySQL:
		ddl = `CREATE TABLE IF NOT EXISTS ` + MetaTable + ` (
  table_name VARCHAR(128) NOT NULL,
  row_pk VARCHAR(255) NOT NULL,
  version BIGINT NOT NULL,
  updated_at DATETIME(3) NOT NULL,
  PRIMARY KEY (table_name, row_pk)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
	case DriverPostgres:
		ddl = `CREATE TABLE IF NOT EXISTS ` + MetaTable + ` (
  table_name TEXT NOT NULL,
  row_pk TEXT NOT NULL,
  version BIGINT NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (table_name, row_pk)
);`
	}
	_, err := db.ExecContext(ctx, ddl)
	return err
}

// InstallTriggers：在业务表上挂触发器，写入 outbox（实时捕获变更）。
func InstallTriggers(ctx context.Context, db *sql.DB, driver Driver, table, pkCol string) error {
	table = strings.TrimSpace(table)
	pkCol = strings.TrimSpace(pkCol)
	if table == "" || pkCol == "" {
		return fmt.Errorf("table and pk required")
	}
	switch driver {
	case DriverSQLite:
		return installSQLiteTriggers(ctx, db, table, pkCol)
	case DriverMySQL:
		return installMySQLTriggers(ctx, db, table, pkCol)
	case DriverPostgres:
		return installPostgresTriggers(ctx, db, table, pkCol)
	default:
		return fmt.Errorf("unsupported driver")
	}
}

func installSQLiteTriggers(ctx context.Context, db *sql.DB, table, pk string) error {
	if err := EnsureFlagTable(ctx, db, DriverSQLite); err != nil {
		return err
	}
	guard := `(SELECT IFNULL((SELECT v FROM ` + FlagTable + ` WHERE k='applying'),0))=0`
	stmts := []string{
		fmt.Sprintf(`DROP TRIGGER IF EXISTS ajz_sync_ai_%s`, table),
		fmt.Sprintf(`DROP TRIGGER IF EXISTS ajz_sync_au_%s`, table),
		fmt.Sprintf(`DROP TRIGGER IF EXISTS ajz_sync_ad_%s`, table),
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_ai_%s AFTER INSERT ON "%s" WHEN %s BEGIN
  INSERT INTO %s(table_name,row_pk,op,payload,version,created_at)
  VALUES('%s', CAST(NEW."%s" AS TEXT), 'upsert', '',
    CAST(strftime('%%s','now') AS INTEGER)*1000, datetime('now'));
END;`, table, table, guard, OutboxTable, table, pk),
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_au_%s AFTER UPDATE ON "%s" WHEN %s BEGIN
  INSERT INTO %s(table_name,row_pk,op,payload,version,created_at)
  VALUES('%s', CAST(NEW."%s" AS TEXT), 'upsert', '',
    CAST(strftime('%%s','now') AS INTEGER)*1000, datetime('now'));
END;`, table, table, guard, OutboxTable, table, pk),
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_ad_%s AFTER DELETE ON "%s" WHEN %s BEGIN
  INSERT INTO %s(table_name,row_pk,op,payload,version,created_at)
  VALUES('%s', CAST(OLD."%s" AS TEXT), 'delete', '',
    CAST(strftime('%%s','now') AS INTEGER)*1000, datetime('now'));
END;`, table, table, guard, OutboxTable, table, pk),
	}
	for _, s := range stmts {
		if _, err := db.ExecContext(ctx, s); err != nil {
			return fmt.Errorf("sqlite trigger %s: %w", table, err)
		}
	}
	return nil
}

func installMySQLTriggers(ctx context.Context, db *sql.DB, table, pk string) error {
	drops := []string{
		fmt.Sprintf("DROP TRIGGER IF EXISTS ajz_sync_ai_%s", table),
		fmt.Sprintf("DROP TRIGGER IF EXISTS ajz_sync_au_%s", table),
		fmt.Sprintf("DROP TRIGGER IF EXISTS ajz_sync_ad_%s", table),
	}
	for _, s := range drops {
		_, _ = db.ExecContext(ctx, s)
	}
	creates := []string{
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_ai_%s AFTER INSERT ON `+"`%s`"+` FOR EACH ROW
BEGIN
  IF IFNULL(@ajz_applying,0)=0 THEN
    INSERT INTO %s(table_name,row_pk,op,payload,version)
    VALUES('%s', CAST(NEW.`+"`%s`"+` AS CHAR), 'upsert', '', UNIX_TIMESTAMP(NOW(3))*1000);
  END IF;
END`, table, table, OutboxTable, table, pk),
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_au_%s AFTER UPDATE ON `+"`%s`"+` FOR EACH ROW
BEGIN
  IF IFNULL(@ajz_applying,0)=0 THEN
    INSERT INTO %s(table_name,row_pk,op,payload,version)
    VALUES('%s', CAST(NEW.`+"`%s`"+` AS CHAR), 'upsert', '', UNIX_TIMESTAMP(NOW(3))*1000);
  END IF;
END`, table, table, OutboxTable, table, pk),
		fmt.Sprintf(`CREATE TRIGGER ajz_sync_ad_%s AFTER DELETE ON `+"`%s`"+` FOR EACH ROW
BEGIN
  IF IFNULL(@ajz_applying,0)=0 THEN
    INSERT INTO %s(table_name,row_pk,op,payload,version)
    VALUES('%s', CAST(OLD.`+"`%s`"+` AS CHAR), 'delete', '', UNIX_TIMESTAMP(NOW(3))*1000);
  END IF;
END`, table, table, OutboxTable, table, pk),
	}
	for _, s := range creates {
		if _, err := db.ExecContext(ctx, s); err != nil {
			return fmt.Errorf("mysql trigger %s: %w", table, err)
		}
	}
	return nil
}

func installPostgresTriggers(ctx context.Context, db *sql.DB, table, pk string) error {
	fn := fmt.Sprintf("ajz_sync_fn_%s", table)
	_, err := db.ExecContext(ctx, fmt.Sprintf(`
CREATE OR REPLACE FUNCTION %s() RETURNS trigger AS $$
BEGIN
  IF COALESCE(current_setting('ajz.applying', true), '0') = '1' THEN
    IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
  END IF;
  IF TG_OP = 'DELETE' THEN
    INSERT INTO %s(table_name,row_pk,op,payload,version)
    VALUES('%s', OLD.%s::text, 'delete', '', (EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint);
    RETURN OLD;
  ELSE
    INSERT INTO %s(table_name,row_pk,op,payload,version)
    VALUES('%s', NEW.%s::text, 'upsert', '', (EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint);
    RETURN NEW;
  END IF;
END;
$$ LANGUAGE plpgsql;`, fn, OutboxTable, table, quoteIdent(DriverPostgres, pk), OutboxTable, table, quoteIdent(DriverPostgres, pk)))
	if err != nil {
		return err
	}
	_, _ = db.ExecContext(ctx, fmt.Sprintf(`DROP TRIGGER IF EXISTS ajz_sync_trg_%s ON %s`, table, quoteIdent(DriverPostgres, table)))
	_, err = db.ExecContext(ctx, fmt.Sprintf(`
CREATE TRIGGER ajz_sync_trg_%s
AFTER INSERT OR UPDATE OR DELETE ON %s
FOR EACH ROW EXECUTE PROCEDURE %s();`, table, quoteIdent(DriverPostgres, table), fn))
	return err
}

func ListTables(ctx context.Context, db *sql.DB, driver Driver) ([]string, error) {
	var q string
	switch driver {
	case DriverSQLite:
		q = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_ajz_%' ORDER BY name`
	case DriverMySQL:
		q = `SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name NOT LIKE '\_ajz\_%' ORDER BY table_name`
	case DriverPostgres:
		q = `SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename NOT LIKE '\_ajz\_%' ORDER BY tablename`
	default:
		return nil, fmt.Errorf("unsupported")
	}
	rows, err := db.QueryContext(ctx, q)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var n string
		if err := rows.Scan(&n); err != nil {
			return nil, err
		}
		out = append(out, n)
	}
	return out, rows.Err()
}
