package db

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"
)

func (s *Store) CopySQLiteToRemote() (string, error) {
	result, err := s.ImportSQLiteToRemote()
	return result.FinishedAt, err
}

func (s *Store) CopyRemoteToSQLite() (string, error) {
	result, err := s.SyncNow()
	return result.FinishedAt, err
}

func (s *Store) ImportSQLiteToRemote() (SyncResult, error) {
	if !s.trySyncLock() {
		return SyncResult{Direction: "sqlite_to_remote", Status: "running", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"database sync is already running"}}, errors.New("database sync is already running")
	}
	defer s.syncMu.Unlock()
	s.mu.RLock()
	remote := s.remoteDB
	remoteDialect := s.remoteDialect
	local := s.localDB
	localDialect := s.localDialect
	s.mu.RUnlock()
	if remote == nil {
		result := SyncResult{Direction: "sqlite_to_remote", Status: "skipped", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"remote database is not configured"}}
		s.setSyncStatus(result, nil)
		return result, nil
	}
	result, err := copyAllTables(local, localDialect, remote, remoteDialect, "sqlite_to_remote")
	s.setSyncStatus(result, err)
	return result, err
}

func (s *Store) SyncNow() (SyncResult, error) {
	if !s.trySyncLock() {
		return SyncResult{Direction: "remote_to_sqlite", Status: "running", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"database sync is already running"}}, errors.New("database sync is already running")
	}
	defer s.syncMu.Unlock()
	s.mu.RLock()
	remote := s.remoteDB
	remoteDialect := s.remoteDialect
	local := s.localDB
	localDialect := s.localDialect
	s.mu.RUnlock()
	if remote == nil {
		result := SyncResult{Direction: "remote_to_sqlite", Status: "skipped", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"remote database is not configured"}}
		s.setSyncStatus(result, nil)
		return result, nil
	}
	result, err := copyAllTables(remote, remoteDialect, local, localDialect, "remote_to_sqlite")
	s.setSyncStatus(result, err)
	return result, err
}

func (s *Store) setSyncStatus(result SyncResult, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if err != nil {
		s.status.LastSyncAt = result.FinishedAt
		s.status.LastSyncError = err.Error()
		return
	}
	s.status.LastSyncAt = result.FinishedAt
	s.status.LastSyncError = ""
}

func (s *Store) trySyncLock() bool {
	return s.syncMu.TryLock()
}

type tableSpec struct {
	Name     string
	Columns  []string
	Conflict []string
}

var syncTables = []tableSpec{
	{"schema_migrations", []string{"version", "applied_at", "description"}, []string{"version"}},
	{"admin_users", []string{"id", "username", "password_hash", "password_changed", "created_at", "updated_at"}, []string{"id"}},
	{"release_packages", []string{"id", "product", "version", "platform", "arch", "file_name", "url", "sha256", "size_bytes", "enabled", "created_at", "updated_at"}, []string{"id"}},
	{"release_notices", []string{"id", "version", "build", "channel", "title", "message", "release_notes", "message_md", "release_notes_md", "download_url", "notice_file", "raw_json", "published_at", "created_at", "updated_at"}, []string{"id"}},
	{"release_notice_revisions", []string{"id", "version", "raw_json", "note", "created_by", "created_at"}, []string{"id"}},
	{"feedback_tickets", []string{"code", "title", "type", "severity", "category", "priority", "contact", "body", "status", "status_detail", "public_reply", "note", "assignee", "handled_by", "due_at", "resolved_at", "archived_at", "sla_level", "source_channel", "risk_score", "resolution", "attachment", "package_path", "encrypted_package_path", "package_sha256", "plain_package_sha256", "summary_text", "included_files", "mail_sent", "remote_addr", "tags", "created_at", "updated_at", "last_activity_at"}, []string{"code"}},
	{"feedback_comments", []string{"id", "feedback_code", "author", "body", "internal", "created_at"}, []string{"id"}},
	{"feedback_attachments", []string{"id", "feedback_code", "kind", "path", "file_name", "sha256", "size_bytes", "created_at"}, []string{"id"}},
	{"feedback_events", []string{"id", "feedback_code", "event_type", "actor", "from_value", "to_value", "message", "created_at"}, []string{"id"}},
	{"feedback_tags", []string{"feedback_code", "tag", "created_at"}, []string{"feedback_code", "tag"}},
	{"mail_records", []string{"id", "feedback_code", "kind", "status", "to_address", "subject", "plain_body", "html_body", "attachment_path", "attachment_name", "error_message", "created_at", "sent_at"}, []string{"id"}},
	{"source_categories", []string{"id", "category_id", "name", "enabled", "ui_config", "created_at", "updated_at"}, []string{"id"}},
	{"source_endpoints", []string{"id", "category_id", "category_name", "source_id", "name", "description", "method", "api_url", "url_template", "thumbnail_url", "proxy_mode", "timeout_ms", "retry_count", "cache_seconds", "check_interval_sec", "enabled", "client_visible", "supported_formats", "last_status", "last_latency_ms", "last_checked_at", "last_error", "consecutive_failure", "created_at", "updated_at"}, []string{"id"}},
	{"endpoint_health_checks", []string{"id", "source_db_id", "status", "latency_ms", "error", "checked_at"}, []string{"id"}},
	{"endpoint_call_logs", []string{"id", "source_id", "status", "latency_ms", "error", "client", "created_at"}, []string{"id"}},
	{"system_settings", []string{"key", "value", "updated_at"}, []string{"key"}},
	{"audit_logs", []string{"id", "actor", "type", "target", "message", "ip", "user_agent", "created_at"}, []string{"id"}},
	{"legacy_json_revisions", []string{"id", "name", "raw", "note", "created_by", "created_at"}, []string{"id"}},
	{"webhook_deliveries", []string{"id", "webhook_name", "event", "status", "attempts", "response_code", "error_message", "payload_sha256", "created_at", "finished_at"}, []string{"id"}},
	{"legacy_sync_jobs", []string{"id", "status", "summary", "stats_json", "started_at", "finished_at"}, []string{"id"}},
}

func copyAllTables(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, direction string) (SyncResult, error) {
	result := SyncResult{Direction: direction, Status: "completed", Tables: map[string]int{}, FinishedAt: Now()}
	for _, table := range syncTables {
		count, err := copyTable(src, srcDialect, dst, dstDialect, table)
		if err != nil {
			result.Status = "failed"
			result.FinishedAt = Now()
			return result, err
		}
		result.Tables[table.Name] = count
	}
	result.FinishedAt = Now()
	return result, nil
}

func copyTable(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, spec tableSpec) (int, error) {
	rows, err := src.Query(srcDialect.rebind("SELECT " + srcDialect.columnList(spec.Columns) + " FROM " + spec.Name))
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	insertSQL := dstDialect.rebind(dstDialect.upsert(spec.Name, spec.Columns, spec.Conflict))
	count := 0
	for rows.Next() {
		values := make([]any, len(spec.Columns))
		ptrs := make([]any, len(spec.Columns))
		for index := range values {
			ptrs[index] = &values[index]
		}
		if err := rows.Scan(ptrs...); err != nil {
			return count, err
		}
		for index, value := range values {
			if bytes, ok := value.([]byte); ok {
				values[index] = string(bytes)
			}
		}
		if _, err := dst.Exec(insertSQL, values...); err != nil {
			return count, err
		}
		count++
	}
	return count, rows.Err()
}

func readPrototypeState(path string) (*state, error) {
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) || len(data) == 0 {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	trimmed := strings.TrimSpace(string(data))
	if !strings.HasPrefix(trimmed, "{") {
		return nil, nil
	}
	var prototype state
	if err := json.Unmarshal(data, &prototype); err != nil {
		return nil, fmt.Errorf("existing sqlite path is not a valid sqlite database or JSON prototype: %w", err)
	}
	return &prototype, nil
}

func backupPrototypeFile(path string) error {
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) || len(data) == 0 {
		return nil
	}
	if err != nil {
		return err
	}
	if !strings.HasPrefix(strings.TrimSpace(string(data)), "{") {
		return nil
	}
	backup := path + ".json-prototype-" + time.Now().UTC().Format("20060102-150405") + ".bak"
	if err := os.WriteFile(backup, data, 0o640); err != nil {
		return err
	}
	return os.Remove(path)
}

func (s *Store) importPrototype(prototype state) error {
	for _, admin := range prototype.Admins {
		if admin.CreatedAt == "" {
			admin.CreatedAt = Now()
		}
		if admin.UpdatedAt == "" {
			admin.UpdatedAt = admin.CreatedAt
		}
		_, _ = s.exec(`INSERT INTO admin_users (id, username, password_hash, password_changed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
			admin.ID, admin.Username, admin.PasswordHash, boolInt(admin.PasswordChanged), admin.CreatedAt, admin.UpdatedAt)
	}
	for _, item := range prototype.Feedbacks {
		_ = s.InsertFeedback(item)
	}
	for _, item := range prototype.Sources {
		_, _ = s.UpsertSource(item)
	}
	for _, item := range prototype.SourceChecks {
		_ = s.RecordSourceCheck(item.SourceID, item.Status, item.LatencyMS, item.Error)
	}
	for _, item := range prototype.SourceCalls {
		_ = s.RecordSourceCall(item)
	}
	for _, item := range prototype.AuditLogs {
		_ = s.InsertAudit(item)
	}
	return nil
}
