package com.yimeng.okr.config;

import com.yimeng.okr.service.DatabaseBackupService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

@Slf4j
@Component
@RequiredArgsConstructor
@Order(2)
public class DatabaseMigrationRunner implements CommandLineRunner {

    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;
    private final DatabaseBackupService databaseBackupService;

    @Override
    public void run(String... args) {
        jdbcTemplate.execute("""
                CREATE TABLE IF NOT EXISTS schema_migration (
                    version INTEGER PRIMARY KEY,
                    description TEXT NOT NULL,
                    applied_at TEXT NOT NULL
                )
                """);

        List<DatabaseMigration> pending = migrations().stream()
                .filter(m -> !isApplied(m.version()))
                .toList();

        if (pending.isEmpty()) {
            return;
        }

        int firstVersion = pending.get(0).version();
        log.info("Preparing backup before migration v{}", firstVersion);
        databaseBackupService.createBackup("pre-migration-v" + firstVersion);

        MigrationContext context = new MigrationContext(jdbcTemplate);
        for (DatabaseMigration migration : pending) {
            log.info("Applying database migration {} - {}", migration.version(), migration.description());
            try {
                transactionTemplate.executeWithoutResult(status -> {
                    migration.apply(context);
                    jdbcTemplate.update("""
                            INSERT INTO schema_migration(version, description, applied_at)
                            VALUES (?, ?, datetime('now'))
                            """, migration.version(), migration.description());
                });
                log.info("Database migration {} applied", migration.version());
            } catch (Exception e) {
                log.error("Database migration {} failed and was rolled back: {}", migration.version(), e.getMessage());
                throw new RuntimeException("Database migration v" + migration.version() + " failed: " + e.getMessage(), e);
            }
        }
    }

    private boolean isApplied(int version) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM schema_migration WHERE version = ?",
                Integer.class, version);
        return count != null && count > 0;
    }

    private List<DatabaseMigration> migrations() {
        return List.of(new DatabaseMigration(1, "baseline compatibility columns and workflow tables", context -> {
            context.addColumnIfMissing("sys_user", "scopes", "TEXT DEFAULT '[]'");
            context.addColumnIfMissing("sys_user", "status", "VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'");
            context.addColumnIfMissing("sys_user", "disabled_at", "TEXT");
            context.addColumnIfMissing("sys_user", "left_at", "TEXT");

            context.addColumnIfMissing("okr_objective", "level", "VARCHAR(16) NOT NULL DEFAULT 'INDIVIDUAL'");
            context.addColumnIfMissing("okr_objective", "parent_objective_id", "INTEGER DEFAULT NULL");
            context.addColumnIfMissing("okr_objective", "sort_order", "INTEGER DEFAULT 0");
            context.addColumnIfMissing("okr_objective", "progress", "REAL DEFAULT 0");

            context.addColumnIfMissing("performance_score", "status", "VARCHAR(32)");
            context.addColumnIfMissing("performance_score", "published_at", "TEXT");
            context.addColumnIfMissing("performance_score", "confirmed_at", "TEXT");

            context.addColumnIfMissing("performance_feedback", "status", "VARCHAR(32)");
            context.addColumnIfMissing("performance_feedback", "closed_at", "TEXT");

            context.execute("""
                    CREATE TABLE IF NOT EXISTS period_participant (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        period_id INTEGER NOT NULL,
                        user_id INTEGER NOT NULL,
                        status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
                        reason TEXT,
                        operated_by INTEGER,
                        exempted_at TEXT,
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL
                    )
                    """);
            context.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_period_participant ON period_participant(period_id, user_id)");

            context.execute("""
                    CREATE TABLE IF NOT EXISTS score_appeal (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        period_id INTEGER NOT NULL,
                        user_id INTEGER NOT NULL,
                        score_id INTEGER NOT NULL,
                        status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
                        reason TEXT NOT NULL,
                        reply TEXT,
                        submitted_at TEXT,
                        resolved_by INTEGER,
                        resolved_at TEXT,
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL
                    )
                    """);

            context.execute("UPDATE performance_score SET status = 'PUBLISHED' WHERE type = 'FINAL' AND status IS NULL");
        }),

        new DatabaseMigration(2, "structured score items table and performance indexes", context -> {
            context.execute("""
                    CREATE TABLE IF NOT EXISTS performance_score_item (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        score_id INTEGER NOT NULL,
                        item_type VARCHAR(20) NOT NULL,
                        reference_id INTEGER,
                        name VARCHAR(256) NOT NULL,
                        max_score REAL,
                        score REAL NOT NULL DEFAULT 0,
                        description TEXT,
                        sort_order INTEGER DEFAULT 0,
                        snapshot_json TEXT,
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL
                    )
                    """);
            context.execute("CREATE INDEX IF NOT EXISTS idx_score_item_score ON performance_score_item(score_id, sort_order)");

            context.execute("CREATE INDEX IF NOT EXISTS idx_ps_period_user_type ON performance_score(period_id, user_id, type)");
            context.execute("CREATE INDEX IF NOT EXISTS idx_ps_period_status ON performance_score(period_id, status)");
            context.execute("CREATE INDEX IF NOT EXISTS idx_okr_period_user ON okr_objective(period_id, user_id)");
            context.execute("CREATE INDEX IF NOT EXISTS idx_kr_source ON kr_key_result(source_kr_id)");
            context.execute("CREATE INDEX IF NOT EXISTS idx_notif_user_read ON notification(user_id, is_read)");
        }),

        new DatabaseMigration(3, "backfill comments_json into performance_score_item", context -> {
            var rows = context.query("SELECT id, comments_json FROM performance_score WHERE comments_json IS NOT NULL AND comments_json != ''");
            int backfilled = 0;
            int skipped = 0;

            for (var row : rows) {
                Long scoreId = ((Number) row.get("id")).longValue();
                String json = (String) row.get("comments_json");
                try {
                    com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
                    var node = mapper.readTree(json);

                    int sortOrder = 0;

                    var krScores = node.get("krScores");
                    if (krScores != null && krScores.isArray()) {
                        for (var krNode : krScores) {
                            String title = krNode.has("title") ? krNode.get("title").asText("") : "";
                            double score = krNode.has("score") ? krNode.get("score").asDouble() : 0;
                            Long krId = krNode.has("krId") ? krNode.get("krId").asLong() : null;

                            context.update(
                                    "INSERT INTO performance_score_item(score_id, item_type, reference_id, name, max_score, score, sort_order, created_at, updated_at) " +
                                    "VALUES (?, 'KR', ?, ?, 1.0, ?, ?, datetime('now'), datetime('now'))",
                                    scoreId, krId, title, score, sortOrder++);
                        }
                    }

                    var dimensions = node.get("dimensions");
                    if (dimensions != null && dimensions.isArray()) {
                        for (var dimNode : dimensions) {
                            String name = dimNode.has("name") ? dimNode.get("name").asText("") : "";
                            double score = dimNode.has("score") ? dimNode.get("score").asDouble() : 0;
                            double maxScore = dimNode.has("maxScore") ? dimNode.get("maxScore").asDouble() : 0;
                            Long dimId = dimNode.has("id") ? dimNode.get("id").asLong() : null;

                            context.update(
                                    "INSERT INTO performance_score_item(score_id, item_type, reference_id, name, max_score, score, sort_order, created_at, updated_at) " +
                                    "VALUES (?, 'DIMENSION', ?, ?, ?, ?, ?, datetime('now'), datetime('now'))",
                                    scoreId, dimId, name, maxScore, score, sortOrder++);
                        }
                    }

                    backfilled++;
                } catch (Exception e) {
                    log.warn("Cannot parse comments_json for score_id={}, keeping original JSON", scoreId);
                    skipped++;
                }
            }

            if (backfilled == 0 && !rows.isEmpty()) {
                log.warn("No scores could be backfilled from {} rows with comments_json", rows.size());
            }
            log.info("Migration 3 backfill: {} scores processed, {} skipped", backfilled, skipped);
        }),

        new DatabaseMigration(4, "normalize legacy 4x15 dimensions to 4x10 for OKR 60/KPI 40", context -> {
            if (!context.tableExists("assessment_dimension")) return;
            var dims = context.query(
                    "SELECT id, name, max_score FROM assessment_dimension WHERE deleted = 0 ORDER BY sort_order");
            if (dims.size() == 4) {
                boolean all15 = dims.stream().allMatch(
                        d -> ((Number) d.get("max_score")).intValue() == 15);
                if (all15) {
                    for (var dim : dims) {
                        context.update("UPDATE assessment_dimension SET max_score = 10 WHERE id = ?",
                                dim.get("id"));
                    }
                    log.info("Migration 4: Updated default dimensions from 4x15 to 4x10");
                }
            }
            Integer total = context.queryForInt(
                    "SELECT COALESCE(SUM(max_score),0) FROM assessment_dimension WHERE deleted = 0");
            if (total != null && total != 40 && total != 0) {
                log.warn("Migration 4: Dimension total is {} (expected 40). Manual review may be needed.", total);
            }
        }));
    }

    private record DatabaseMigration(int version, String description, Consumer<MigrationContext> operation) {
        void apply(MigrationContext context) {
            operation.accept(context);
        }
    }

    static class MigrationContext {
        private final JdbcTemplate jdbcTemplate;

        MigrationContext(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }

        void execute(String sql) {
            jdbcTemplate.execute(sql);
        }

        List<Map<String, Object>> query(String sql, Object... params) {
            return jdbcTemplate.queryForList(sql, params);
        }

        Integer queryForInt(String sql, Object... params) {
            return jdbcTemplate.queryForObject(sql, Integer.class, params);
        }

        int update(String sql, Object... params) {
            return jdbcTemplate.update(sql, params);
        }

        void addColumnIfMissing(String tableName, String columnName, String definition) {
            if (!tableExists(tableName) || columnExists(tableName, columnName)) {
                return;
            }
            jdbcTemplate.execute("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + definition);
        }

        boolean tableExists(String tableName) {
            Integer count = jdbcTemplate.queryForObject("""
                    SELECT COUNT(*) FROM sqlite_master
                    WHERE type = 'table' AND name = ?
                    """, Integer.class, tableName);
            return count != null && count > 0;
        }

        boolean columnExists(String tableName, String columnName) {
            return jdbcTemplate.queryForList("PRAGMA table_info(" + tableName + ")").stream()
                    .anyMatch(row -> columnName.equalsIgnoreCase(String.valueOf(row.get("name"))));
        }
    }
}
