using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;

namespace ExperionCrawler.Infrastructure.Database;

// ── DbContext ────────────────────────────────────────────────────────────────

public class ExperionDbContext : DbContext
{
    public ExperionDbContext(DbContextOptions<ExperionDbContext> options) : base(options) { }

    public DbSet<ExperionRecord> ExperionRecords => Set<ExperionRecord>();
    public DbSet<RawNodeMap>     RawNodeMaps     => Set<RawNodeMap>();
    public DbSet<NodeMapMaster>  NodeMapMasters  => Set<NodeMapMaster>();
    public DbSet<RealtimePoint>  RealtimePoints  => Set<RealtimePoint>();
    public DbSet<HistoryRecord>  HistoryRecords  => Set<HistoryRecord>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ExperionRecord>(e =>
        {
            e.HasKey(x => x.Id);
            e.HasIndex(x => x.CollectedAt);
            e.HasIndex(x => x.NodeId);
            e.HasIndex(x => x.SessionId);
        });

        modelBuilder.Entity<RawNodeMap>(e =>
        {
            e.HasKey(x => x.Id);
            e.HasIndex(x => x.NodeId);
        });

        modelBuilder.Entity<NodeMapMaster>(e =>
        {
            e.HasKey(x => x.Id);
            e.HasIndex(x => x.NodeId);
            e.HasIndex(x => x.Level);
        });

        modelBuilder.Entity<RealtimePoint>(e =>
        {
            e.HasKey(x => x.Id);
            e.HasIndex(x => x.NodeId).IsUnique();
            e.HasIndex(x => x.TagName);
        });

        modelBuilder.Entity<HistoryRecord>(e =>
        {
            e.HasKey(x => x.Id);
            e.HasIndex(x => x.TagName);
            e.HasIndex(x => x.RecordedAt);
        });
    }
}

// ── Service ──────────────────────────────────────────────────────────────────

public class ExperionDbService : IExperionDbService
{
    private readonly ExperionDbContext _ctx;
    private readonly ILogger<ExperionDbService> _logger;

    public ExperionDbService(ExperionDbContext ctx, ILogger<ExperionDbService> logger)
    {
        _ctx    = ctx;
        _logger = logger;
    }

    public async Task<bool> InitializeAsync()
    {
        try
        {
            await _ctx.Database.EnsureCreatedAsync();

            // TimeScaleDB 확장 활성화
            await _ctx.Database.ExecuteSqlRawAsync("CREATE EXTENSION IF NOT EXISTS timescaledb");

            // EnsureCreatedAsync는 기존 DB에 새 테이블을 추가하지 않으므로
            // raw_node_map / node_map_master 는 DDL로 직접 보장
            await _ctx.Database.ExecuteSqlRawAsync("""
                CREATE TABLE IF NOT EXISTS raw_node_map (
                    id        SERIAL PRIMARY KEY,
                    level     INTEGER NOT NULL,
                    class     TEXT    NOT NULL,
                    name      TEXT    NOT NULL,
                    node_id   TEXT    NOT NULL,
                    data_type TEXT    NOT NULL
                )
                """);

            await _ctx.Database.ExecuteSqlRawAsync("""
                CREATE TABLE IF NOT EXISTS node_map_master (
                    id        SERIAL PRIMARY KEY,
                    level     INTEGER NOT NULL,
                    class     TEXT    NOT NULL,
                    name      TEXT    NOT NULL,
                    node_id   TEXT    NOT NULL,
                    data_type TEXT    NOT NULL
                )
                """);

            // realtime_table 생성 (실시간 모니터링 포인트)
            await _ctx.Database.ExecuteSqlRawAsync("""
                CREATE TABLE IF NOT EXISTS realtime_table (
                    id          SERIAL PRIMARY KEY,
                    tagname     TEXT        NOT NULL,
                    node_id     TEXT        NOT NULL,
                    livevalue   TEXT,
                    timestamp   TIMESTAMPTZ NOT NULL DEFAULT NOW()
                )
                """);

            // realtime_table은 실시간 값 저장용이므로 하이퍼테이블로 변환하지 않음

            // history 테이블은 수동으로 하이퍼테이블 생성 필요
            // CreateHypertableAsync() 메서드를 사용하여 수동 생성 가능
            // 참고: 하이퍼테이블 생성 후 보존 정책, 압축 정책, 연속 집계 설정은
            // CreateHypertableAsync() 메서드에서 선택적으로 설정 가능

            _logger.LogInformation("[ExperionDb] 데이터베이스 초기화 완료 (TimeScaleDB 활성화)");
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[ExperionDb] 초기화 실패");
            return false;
        }
    }

    /// <summary>
    /// history 테이블을 TimeScaleDB 하이퍼테이블로 생성/마이그레이션
    /// </summary>
    /// <param name="hypertableName">생성할 하이퍼테이블 이름 (예: history_hypertable)</param>
    /// <param name="tableName">기존 테이블 이름 (예: history_table, null이면 새 테이블 생성)</param>
    /// <param name="timeColumn">시간 컬럼 이름</param>
    /// <param name="interval">청크 간격 (예: '1 day')</param>
    /// <returns>하이퍼테이블 생성 여부 (true: 생성됨/기존 있음, false: 실패/스킵)</returns>
    private async Task<bool> CreateHistoryHypertableIfNotExistsAsync(
        string hypertableName, string? tableName, string timeColumn, string interval)
    {
        try
        {
            // SQL injection 방지를 위해 식별자 검증
            if (!IsValidSqlIdentifier(hypertableName))
            {
                _logger.LogWarning("[ExperionDb] 하이퍼테이블 이름 '{HypertableName}'이(가) 유효하지 않음", hypertableName);
                return false;
            }

            // 1️⃣ 하이퍼테이블이 이미 존재하면 스킵
            await using var conn = new NpgsqlConnection(_ctx.Database.GetConnectionString());
            await conn.OpenAsync();
            
            await using var cmd1 = new NpgsqlCommand(
                $"SELECT 1 FROM pg_catalog.pg_tables WHERE tablename = '{hypertableName.Replace("'", "''")}' LIMIT 1", conn);
            var result = await cmd1.ExecuteScalarAsync();
            if (result != null)
            {
                _logger.LogInformation("[ExperionDb] 하이퍼테이블 '{HypertableName}' 이미 존재함", hypertableName);
                return true;
            }

            // 2️⃣ 기존 테이블 이름이 제공되면 검증
            if (tableName != null && !IsValidSqlIdentifier(tableName))
            {
                _logger.LogWarning("[ExperionDb] 테이블 이름 '{TableName}'이(가) 유효하지 않음", tableName);
                return false;
            }

            // 3️⃣ 기존 테이블이 존재하면 → 하이퍼테이블로 마이그레이션
            if (tableName != null)
            {
                await using var cmd2 = new NpgsqlCommand(
                    $"SELECT 1 FROM pg_catalog.pg_tables WHERE tablename = '{tableName.Replace("'", "''")}' LIMIT 1", conn);
                result = await cmd2.ExecuteScalarAsync();
                if (result != null)
                {
                    // 데이터가 있는 경우 migrate_data => true 옵션 필요
#pragma warning disable EF1002 // Method 'ExecuteSqlRawAsync' inserts interpolated strings directly into the SQL
                    await _ctx.Database.ExecuteSqlRawAsync(
                        $"SELECT create_hypertable('{tableName}', '{timeColumn}', chunk_time_interval => INTERVAL '{interval}', create_default_indexes => true, migrate_data => true)");
                    _logger.LogInformation("[ExperionDb] 테이블 '{TableName}'을(를) 하이퍼테이블로 변환 완료", tableName);
                    return true;
                }
            }

            // 4️⃣ 기존 테이블이 없으면 → 새 하이퍼테이블 테이블 생성
            // TimeScaleDB 요구사항: 고유 인덱스를 위해서는 partitioning 컬럼이 primary key에 포함되어야 함
#pragma warning disable EF1002 // Method 'ExecuteSqlRawAsync' inserts interpolated strings directly into the SQL
            await _ctx.Database.ExecuteSqlRawAsync($"""
                CREATE TABLE IF NOT EXISTS {hypertableName} (
                    id          SERIAL,
                    tagname     TEXT        NOT NULL,
                    node_id     TEXT,
                    value       TEXT,
                    livevalue   TEXT,
                    {timeColumn} TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                    PRIMARY KEY (id, {timeColumn})
                )
                """);
            await _ctx.Database.ExecuteSqlRawAsync($"""
                SELECT create_hypertable('{hypertableName}', '{timeColumn}', chunk_time_interval => INTERVAL '{interval}', create_default_indexes => true)
                """);
            _logger.LogInformation("[ExperionDb] 새 하이퍼테이블 '{HypertableName}' 생성 완료", hypertableName);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[ExperionDb] 하이퍼테이블 '{HypertableName}' 생성 실패 (기본 테이블 사용 계속)", hypertableName);
            return false;
        }
    }

    public async Task<int> SaveRecordsAsync(IEnumerable<ExperionRecord> records)
    {
        var list = records.ToList();
        await _ctx.ExperionRecords.AddRangeAsync(list);
        var saved = await _ctx.SaveChangesAsync();
        _logger.LogInformation("[ExperionDb] {Count}건 저장", saved);
        return saved;
    }

    public async Task<int> ClearRecordsAsync()
    {
        var deleted = await _ctx.ExperionRecords.ExecuteDeleteAsync();
        _logger.LogInformation("[ExperionDb] {Count}건 삭제 (초기화)", deleted);
        return deleted;
    }

    public async Task<int> BuildMasterFromRawAsync(bool truncate = false)
    {
        if (truncate)
        {
            await _ctx.Database.ExecuteSqlRawAsync(
                "TRUNCATE TABLE node_map_master RESTART IDENTITY");
            _logger.LogInformation("[ExperionDb] node_map_master 초기화 완료");
        }

        var inserted = await _ctx.Database.ExecuteSqlRawAsync(
            "INSERT INTO node_map_master (level, class, name, node_id, data_type) " +
            "SELECT level, class, name, node_id, data_type FROM raw_node_map");

        _logger.LogInformation("[ExperionDb] node_map_master 빌드 완료: {Count}건", inserted);
        return inserted;
    }

    public async Task<IEnumerable<ExperionRecord>> GetRecordsAsync(
        DateTime? from = null, DateTime? to = null, int limit = 1000)
    {
        var q = _ctx.ExperionRecords.AsQueryable();
        if (from.HasValue) q = q.Where(r => r.CollectedAt >= from.Value);
        if (to.HasValue)   q = q.Where(r => r.CollectedAt <= to.Value);
        return await q.OrderByDescending(r => r.CollectedAt).Take(limit).ToListAsync();
    }

    public async Task<int> GetTotalCountAsync()
        => await _ctx.ExperionRecords.CountAsync();

    public async Task<IEnumerable<string>> GetNameListAsync()
    {
        return await _ctx.NodeMapMasters
            .Select(x => x.Name).Distinct()
            .OrderBy(x => x).ToListAsync();
    }

    public async Task<NodeMapStats> GetMasterStatsAsync()
    {
        if (!await _ctx.NodeMapMasters.AnyAsync())
            return new NodeMapStats(0, 0, 0, 0, Enumerable.Empty<string>());

        var total         = await _ctx.NodeMapMasters.CountAsync();
        var objectCount   = await _ctx.NodeMapMasters.CountAsync(x => x.Class == "Object");
        var variableCount = await _ctx.NodeMapMasters.CountAsync(x => x.Class == "Variable");
        var maxLevel      = await _ctx.NodeMapMasters.MaxAsync(x => (int?)x.Level) ?? 0;
        var dataTypes     = await _ctx.NodeMapMasters
                                .Select(x => x.DataType).Distinct()
                                .OrderBy(x => x).ToListAsync();

        _logger.LogInformation("[ExperionDb] 노드맵 통계: total={Total}", total);
        return new NodeMapStats(total, objectCount, variableCount, maxLevel, dataTypes);
    }

    // ── RealtimeTable ─────────────────────────────────────────────────────────

    private static string ExtractTagName(string nodeId)
    {
        var idx = nodeId.LastIndexOf(':');
        return idx >= 0 ? nodeId[(idx + 1)..] : nodeId;
    }

    public async Task<int> BuildRealtimeTableAsync(
        IEnumerable<string> names, IEnumerable<string> dataTypes)
    {
        var nameList  = names.Where(n => !string.IsNullOrEmpty(n)).ToList();
        var dtList    = dataTypes.Where(d => !string.IsNullOrEmpty(d)).ToList();

        var q = _ctx.NodeMapMasters.AsQueryable();
        if (nameList.Count > 0) q = q.Where(x => nameList.Contains(x.Name));
        if (dtList.Count   > 0) q = q.Where(x => dtList.Contains(x.DataType));

        var sources = await q.ToListAsync();

        await _ctx.Database.ExecuteSqlRawAsync(
            "TRUNCATE TABLE realtime_table RESTART IDENTITY");

        var points = sources.Select(s => new RealtimePoint
        {
            TagName   = ExtractTagName(s.NodeId),
            NodeId    = s.NodeId,
            LiveValue = null,
            Timestamp = DateTime.UtcNow
        }).ToList();

        await _ctx.RealtimePoints.AddRangeAsync(points);
        var saved = await _ctx.SaveChangesAsync();
        _logger.LogInformation("[ExperionDb] realtime_table 빌드: {Count}건", saved);
        return saved;
    }

    public async Task<IEnumerable<RealtimePoint>> GetRealtimePointsAsync()
        => await _ctx.RealtimePoints.OrderBy(x => x.TagName).ToListAsync();

    public async Task<RealtimePoint> AddRealtimePointAsync(string nodeId)
    {
        var existing = await _ctx.RealtimePoints.FirstOrDefaultAsync(x => x.NodeId == nodeId);
        if (existing != null) return existing;

        var point = new RealtimePoint
        {
            TagName   = ExtractTagName(nodeId),
            NodeId    = nodeId,
            LiveValue = null,
            Timestamp = DateTime.UtcNow
        };
        _ctx.RealtimePoints.Add(point);
        await _ctx.SaveChangesAsync();
        _logger.LogInformation("[ExperionDb] 수동 추가: {NodeId}", nodeId);
        return point;
    }

    public async Task<bool> DeleteRealtimePointAsync(int id)
    {
        var point = await _ctx.RealtimePoints.FindAsync(id);
        if (point == null) return false;
        _ctx.RealtimePoints.Remove(point);
        await _ctx.SaveChangesAsync();
        return true;
    }

    public async Task<int> UpdateLiveValueAsync(string nodeId, string? value, DateTime timestamp)
    {
        return await _ctx.RealtimePoints
            .Where(x => x.NodeId == nodeId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(x => x.LiveValue, value)
                .SetProperty(x => x.Timestamp, timestamp));
    }

    public async Task<int> BatchUpdateLiveValuesAsync(IEnumerable<LiveValueUpdate> updates)
    {
        var list = updates.ToList();
        if (list.Count == 0) return 0;

        // 단일 DbContext(단일 DB 커넥션)에서 순차 업데이트 — 커넥션 폭발 방지
        int total = 0;
        foreach (var u in list)
        {
            total += await _ctx.RealtimePoints
                .Where(x => x.NodeId == u.NodeId)
                .ExecuteUpdateAsync(s => s
                    .SetProperty(x => x.LiveValue, u.Value)
                    .SetProperty(x => x.Timestamp, u.Timestamp));
        }
        return total;
    }

    // ── HistoryTable ──────────────────────────────────────────────────────────

    public async Task<int> SnapshotToHistoryAsync()
    {
        var now    = DateTime.UtcNow;
        var points = await _ctx.RealtimePoints.ToListAsync();
        if (points.Count == 0) return 0;

        var rows = points.Select(p => new HistoryRecord
        {
            TagName    = p.TagName,
            NodeId     = p.NodeId,
            Value      = p.LiveValue,
            RecordedAt = now
        }).ToList();

        await _ctx.HistoryRecords.AddRangeAsync(rows);
        var saved = await _ctx.SaveChangesAsync();
        _logger.LogDebug("[ExperionDb] history 스냅샷: {Count}건 @ {Time:HH:mm:ss}", saved, now);
        return saved;
    }

    public async Task<IEnumerable<string>> GetTagNamesAsync()
        => await _ctx.RealtimePoints.Select(x => x.TagName).OrderBy(x => x).ToListAsync();

    public async Task<HistoryQueryResult> QueryHistoryAsync(
        IEnumerable<string> tagNames, DateTime? from, DateTime? to, int limit)
    {
        var tags = tagNames.Where(t => !string.IsNullOrEmpty(t)).ToList();

        var q = _ctx.HistoryRecords.AsQueryable();
        if (tags.Count > 0) q = q.Where(x => tags.Contains(x.TagName));
        if (from.HasValue)  q = q.Where(x => x.RecordedAt >= from.Value);
        if (to.HasValue)    q = q.Where(x => x.RecordedAt <= to.Value);

        var rows = await q.OrderBy(x => x.RecordedAt)
                          .Take(Math.Min(limit, 5000))
                          .ToListAsync();

        // recorded_at 기준으로 행을 묶어서 pivot 구성
        var grouped = rows
            .GroupBy(x => x.RecordedAt)
            .Select(g => new HistoryRow(
                g.Key,
                g.GroupBy(r => r.TagName)
                 .ToDictionary(tg => tg.Key, tg => tg.Last().Value)
                    as IReadOnlyDictionary<string, string?>))
            .ToList();

        var usedTags = tags.Count > 0
            ? tags
            : rows.Select(x => x.TagName).Distinct().OrderBy(x => x).ToList();

        return new HistoryQueryResult(usedTags, grouped);
    }

    public async Task<NodeMapQueryResult> QueryMasterAsync(
        int? minLevel, int? maxLevel, string? nodeClass,
        IEnumerable<string>? names, string? nodeId, string? dataType,
        int limit, int offset)
    {
        var q = _ctx.NodeMapMasters.AsQueryable();

        if (minLevel.HasValue)                 q = q.Where(x => x.Level >= minLevel.Value);
        if (maxLevel.HasValue)                 q = q.Where(x => x.Level <= maxLevel.Value);
        if (!string.IsNullOrEmpty(nodeClass))  q = q.Where(x => x.Class == nodeClass);
        var nameList = names?.Where(n => !string.IsNullOrEmpty(n)).ToList();
        if (nameList?.Count > 0)               q = q.Where(x => nameList.Contains(x.Name));
        if (!string.IsNullOrEmpty(nodeId))     q = q.Where(x => x.NodeId.Contains(nodeId));
        if (!string.IsNullOrEmpty(dataType))   q = q.Where(x => x.DataType == dataType);

        var total = await q.CountAsync();
        var items = await q.OrderBy(x => x.Level).ThenBy(x => x.Name)
                           .Skip(offset).Take(Math.Min(limit, 500))
                           .ToListAsync();

        return new NodeMapQueryResult(total, items);
    }

    /// <summary>realtime_table × node_map_master 조인 → nodeId → dataType 사전 반환</summary>
    public async Task<IReadOnlyDictionary<string, string>> GetRealtimeNodeDataTypesAsync()
    {
        var result = await (
            from rt in _ctx.RealtimePoints
            join nm in _ctx.NodeMapMasters on rt.NodeId equals nm.NodeId into joined
            from nm in joined.DefaultIfEmpty()
            select new { rt.NodeId, DataType = nm == null ? "String" : nm.DataType }
        ).ToListAsync();

        return result
            .GroupBy(x => x.NodeId)
            .ToDictionary(g => g.Key, g => g.First().DataType);
    }

    /// <summary>
    /// 하이퍼테이블 상태 조회합니다.
    /// 하이퍼테이블인지 여부, 레코드 수, 보존 정책, 압축, 연속 집계 설정 등을 확인합니다.
    /// </summary>
    public async Task<HypertableStatusInfo> GetHypertableStatusAsync()
    {
        try
        {
            await _ctx.Database.GetDbConnection().OpenAsync();
            
            // 하이퍼테이블 존재 여부 확인
            await using var hypertableCheckCmd = _ctx.Database.GetDbConnection().CreateCommand();
            hypertableCheckCmd.CommandText = @"
                SELECT EXISTS (
                    SELECT 1 FROM timescaledb_information.hypertables
                    WHERE hypertable_name = 'history_table'
                )";

            var isHypertableResult = Convert.ToBoolean(await hypertableCheckCmd.ExecuteScalarAsync());

            // 레코드 수 조회
            var recordCount = 0;
            if (isHypertableResult)
            {
                await using var countCmd = _ctx.Database.GetDbConnection().CreateCommand();
                countCmd.CommandText = "SELECT COUNT(*) FROM history_table";
                recordCount = Convert.ToInt32(await countCmd.ExecuteScalarAsync());
            }

            // 보존 정책 확인 (pg_extension으로 TimeScaleDB 활성화 여부만 확인)
            var hasRetentionPolicy = false;
            try
            {
                await using var retentionCmd = _ctx.Database.GetDbConnection().CreateCommand();
                retentionCmd.CommandText = @"
                    SELECT EXISTS (
                        SELECT 1 FROM timescaledb_information.policies
                        WHERE policy_type = 'data_retention'
                        AND hypertable_name = 'history_table'
                    )";
                hasRetentionPolicy = Convert.ToBoolean(await retentionCmd.ExecuteScalarAsync());
            }
            catch
            {
                // policies 뷰가 없는 경우 false 유지
                hasRetentionPolicy = false;
            }

            // 압축 확인
            var hasCompression = false;
            try
            {
                await using var compressionCmd = _ctx.Database.GetDbConnection().CreateCommand();
                compressionCmd.CommandText = @"
                    SELECT is_compressed = 't'
                    FROM timescaledb_information.hypertables
                    WHERE hypertable_name = 'history_table'";
                var compressionResult = await compressionCmd.ExecuteScalarAsync();
                if (compressionResult != null)
                {
                    hasCompression = compressionResult.ToString() == "t";
                }
            }
            catch
            {
                // 압축 정보 조회 실패 시 false 유지
                hasCompression = false;
            }

            // 연속 집계 확인
            var hasContinuousAggregate = false;
            try
            {
                await using var aggregateCmd = _ctx.Database.GetDbConnection().CreateCommand();
                aggregateCmd.CommandText = @"
                    SELECT EXISTS (
                        SELECT 1 FROM timescaledb_information.continuous_aggregates
                        WHERE hypertable_name = 'history_table'
                    )";
                hasContinuousAggregate = Convert.ToBoolean(await aggregateCmd.ExecuteScalarAsync());
            }
            catch
            {
                // continuous_aggregates 뷰가 없는 경우 false 유지
                hasContinuousAggregate = false;
            }

            return new HypertableStatusInfo
            {
                IsHypertable = isHypertableResult,
                TableName = "history_table",
                StatusMessage = isHypertableResult
                    ? "하이퍼테이블이 활성화되어 있습니다."
                    : "일반 테이블입니다. CreateHypertableAsync()를 사용하여 하이퍼테이블로 변환할 수 있습니다.",
                RecordCount = recordCount,
                HasRetentionPolicy = hasRetentionPolicy,
                HasCompression = hasCompression,
                HasContinuousAggregate = hasContinuousAggregate
            };
        }
        catch (Exception ex)
        {
            return new HypertableStatusInfo
            {
                IsHypertable = false,
                TableName = "history_table",
                StatusMessage = $"상태 확인 중 오류 발생: {ex.Message}",
                RecordCount = 0,
                HasRetentionPolicy = false,
                HasCompression = false,
                HasContinuousAggregate = false
            };
        }
    }

    /// <summary>
    /// 수동으로 하이퍼테이블을 생성합니다.
    /// 테이블이 이미 존재하거나 하이퍼테이블로 변환된 경우 예외를 throw합니다.
    /// </summary>
    public async Task<HypertableCreateResult> CreateHypertableAsync(HypertableCreateRequest request)
    {
        // 식별자 검증 - SQL injection 방지
        if (!IsValidSqlIdentifier(request.TableName))
        {
            return HypertableCreateResult.Failed($"테이블 이름 '{request.TableName}'은 유효하지 않습니다. 영문, 숫자, 언더스코어, 하이픈, 마침표만 사용 가능합니다.");
        }

        if (!IsValidSqlIdentifier(request.TimeColumn))
        {
            return HypertableCreateResult.Failed($"시간 컬럼 이름 '{request.TimeColumn}'은 유효하지 않습니다. 영문, 숫자, 언더스코어, 하이픈, 마침표만 사용 가능합니다.");
        }

        if (!IsValidSqlIdentifier(request.TimeInterval?.Replace(" ", "")))
        {
            return HypertableCreateResult.Failed($"시간 간격 '{request.TimeInterval}'은 유효하지 않습니다.");
        }

        try
        {
            _ctx.Database.GetDbConnection().Open();
            
            // 1. 테이블 존재 여부 확인
            await using var tableCheckCmd = _ctx.Database.GetDbConnection().CreateCommand();
            tableCheckCmd.CommandText = @"
                SELECT EXISTS (
                    SELECT 1 FROM information_schema.tables
                    WHERE table_schema = 'public'
                    AND table_name = @tableName)";
            tableCheckCmd.Parameters.Add(new NpgsqlParameter("@tableName", request.TableName));

            var tableExists = Convert.ToBoolean(await tableCheckCmd.ExecuteScalarAsync());
            if (!tableExists)
            {
                return HypertableCreateResult.Failed($"테이블 '{request.TableName}'이 존재하지 않습니다.");
            }

            // 2. 이미 하이퍼테이블인지 확인
            await using var hypertableCheckCmd = _ctx.Database.GetDbConnection().CreateCommand();
            hypertableCheckCmd.CommandText = @"
                SELECT EXISTS (
                    SELECT 1 FROM timescaledb_information.hypertables
                    WHERE hypertable_name = @tableName)";
            hypertableCheckCmd.Parameters.Add(new NpgsqlParameter("@tableName", request.TableName));

            var isHypertable = Convert.ToBoolean(await hypertableCheckCmd.ExecuteScalarAsync());
            if (isHypertable)
            {
                return HypertableCreateResult.AlreadyExists($"테이블 '{request.TableName}'은 이미 하이퍼테이블입니다.");
            }

            // 3. TimescaleDB 확장 활성화
            await _ctx.Database.ExecuteSqlRawAsync("CREATE EXTENSION IF NOT EXISTS timescaledb");

            // 3-1. 기존 SERIAL PRIMARY KEY 제약사항 제거 (TimeScaleDB 호환성)
            var dropPrimaryKeySql = $"ALTER TABLE {request.TableName} DROP CONSTRAINT IF EXISTS {request.TableName}_pkey";
            Console.WriteLine($"[DEBUG] 기본키 제약사항 제거 SQL: {dropPrimaryKeySql}");
#pragma warning disable EF1002
            await _ctx.Database.ExecuteSqlRawAsync(dropPrimaryKeySql);
#pragma warning restore EF1002

            // 4. 하이퍼테이블 생성 (기존 데이터 마이그레이션 포함)
            var createHypertableSql = $"SELECT create_hypertable('{request.TableName}'::regclass, '{request.TimeColumn}'::text, if_not_exists => TRUE, migrate_data => TRUE)";
            Console.WriteLine($"[DEBUG] 하이퍼테이블 생성 SQL: {createHypertableSql}");
#pragma warning disable EF1002
            await _ctx.Database.ExecuteSqlRawAsync(createHypertableSql);
#pragma warning restore EF1002

            // 4-1. TimeScaleDB 하이퍼테이블에 적합한 새로운 기본키 생성
            var addPrimaryKeySql = $"ALTER TABLE {request.TableName} ADD CONSTRAINT {request.TableName}_pkey PRIMARY KEY ({request.TimeColumn}, id)";
            Console.WriteLine($"[DEBUG] 새 기본키 생성 SQL: {addPrimaryKeySql}");
#pragma warning disable EF1002
            await _ctx.Database.ExecuteSqlRawAsync(addPrimaryKeySql);
#pragma warning restore EF1002

            // 6. 보존 정책 설정 (요청된 경우)
            if (request.SetRetentionPolicy && !string.IsNullOrEmpty(request.RetentionPeriod))
            {
                var retentionSql = $"SELECT add_retention_policy('{request.TableName}'::regclass, INTERVAL '{request.RetentionPeriod}')";
                Console.WriteLine($"[DEBUG] 보존 정책 SQL: {retentionSql}");
#pragma warning disable EF1002
                await _ctx.Database.ExecuteSqlRawAsync(retentionSql);
#pragma warning restore EF1002
            }

            // 7. 압축 정책 설정 (요청된 경우)
            if (request.EnableCompression && !string.IsNullOrEmpty(request.CompressionPeriod))
            {
                var compressionSql = $"SELECT add_compression_policy('{request.TableName}'::regclass, INTERVAL '{request.CompressionPeriod}')";
                Console.WriteLine($"[DEBUG] 압축 정책 SQL: {compressionSql}");
#pragma warning disable EF1002
                await _ctx.Database.ExecuteSqlRawAsync(compressionSql);
#pragma warning restore EF1002
            }

            // 7-1. history_table 컬럼명 검증 (tag_name vs tagname)
            if (request.TableName == "history_table")
            {
                await using var columnCheckCmd = _ctx.Database.GetDbConnection().CreateCommand();
                columnCheckCmd.CommandText = @"
                    SELECT column_name FROM information_schema.columns
                    WHERE table_schema = 'public'
                    AND table_name = 'history_table'
                    AND column_name IN ('tag_name', 'tagname')
                    ORDER BY column_name;";
                
                var columns = new List<string>();
                await using var reader = await columnCheckCmd.ExecuteReaderAsync();
                while (await reader.ReadAsync())
                {
                    columns.Add(reader.GetString(0));
                }
                
                Console.WriteLine($"[DEBUG] history_table 컬럼명 검증: {string.Join(", ", columns)}");
            }

            // 8. 연속 집계 생성 (요청된 경우)
            if (request.CreateContinuousAggregate)
            {
                // 8-1. 기존 MATERIALIZED VIEW 삭제 (별도 실행)
                var dropViewSql = "DROP MATERIALIZED VIEW IF EXISTS history_5min_agg";
                Console.WriteLine($"[DEBUG] 연속 집계 DROP VIEW SQL: {dropViewSql}");
#pragma warning disable EF1002
                await _ctx.Database.ExecuteSqlRawAsync(dropViewSql);
#pragma warning restore EF1002

                // 8-2. 연속 집계 MATERIALIZED VIEW 생성 (별도 실행)
                var createViewSql = $@"
                    CREATE MATERIALIZED VIEW history_5min_agg
                    WITH (timescaledb.continuous) AS
                    SELECT
                        time_bucket(INTERVAL '5 minutes', {request.TimeColumn}) AS time_bucket,
                        tagname,
                        AVG(value) AS avg_value,
                        first(value, {request.TimeColumn}) AS min_value,
                        last(value, {request.TimeColumn}) AS max_value
                    FROM {request.TableName}
                    GROUP BY time_bucket, tagname;";
                
                Console.WriteLine($"[DEBUG] 연속 집계 CREATE VIEW SQL: {createViewSql}");
                
#pragma warning disable EF1002
                Console.WriteLine($"[DEBUG] CREATE VIEW SQL 실행 전: {createViewSql}");
                await _ctx.Database.ExecuteSqlRawAsync(createViewSql);
                Console.WriteLine($"[DEBUG] CREATE VIEW SQL 실행 성공");
#pragma warning restore EF1002

                // 8-3. 연속 집계 정책 추가
                var aggregatePolicySql = $"SELECT add_continuous_aggregate_policy('history_5min_agg', '10 minutes', '1 minute', '5 minutes')";
                
                Console.WriteLine($"[DEBUG] 연속 집계 정책 SQL: {aggregatePolicySql}");
                
#pragma warning disable EF1002
                await _ctx.Database.ExecuteSqlRawAsync(aggregatePolicySql);
#pragma warning restore EF1002
            }

            return HypertableCreateResult.Ok();
        }
        catch (Exception ex)
        {
            var errorDetails = $"하이퍼테이블 생성 실패: {ex.Message}";
            Console.WriteLine($"[ERROR] {errorDetails}");
            if (ex.InnerException != null)
            {
                Console.WriteLine($"[ERROR] InnerException: {ex.InnerException.Message}");
                Console.WriteLine($"[ERROR] InnerException StackTrace: {ex.InnerException.StackTrace}");
            }
            Console.WriteLine($"[ERROR] Full StackTrace: {ex.StackTrace}");
            return HypertableCreateResult.Failed(errorDetails);
        }
    }

    /// <summary>
    /// SQL 식별자로 안전한지 검증합니다. 영문, 숫자, 언더스코어, 하이픈, 마침표만 허용합니다.
    /// EF1002 SQL injection 방지를 위해 DDL 문에서 식별자를 사용할 때 이 메서드로 검증을 필수로 합니다.
    /// </summary>
    private static bool IsValidSqlIdentifier(string identifier)
    {
        if (string.IsNullOrEmpty(identifier))
            return false;

        if (identifier.Length > 63) // PostgreSQL 식별자 최대 길이
            return false;

        // 영문, 숫자, 언더스코어, 하이픈, 마침표만 허용
        return System.Text.RegularExpressions.Regex.IsMatch(
            identifier,
            @"^[a-zA-Z0-9_\-\.]+$");
    }
}

/// <summary>
/// 하이퍼테이블 상태 정보 결과 클래스
/// </summary>
public record HypertableStatusInfo
{
    public bool IsHypertable { get; init; }
    public string? TableName { get; init; }
    public string? StatusMessage { get; init; }
    public int RecordCount { get; init; }
    public bool HasRetentionPolicy { get; init; }
    public bool HasCompression { get; init; }
    public bool HasContinuousAggregate { get; init; }
}

/// <summary>
/// 하이퍼테이블 생성 요청 클래스
/// </summary>
public record HypertableCreateRequest
{
    public string TableName { get; init; } = "history_table";
    public string TimeColumn { get; init; } = "recorded_at";
    public string TimeInterval { get; init; } = "1 day";
    public bool MigrateData { get; init; } = true;
    public bool SetRetentionPolicy { get; init; } = true;
    public string RetentionPeriod { get; init; } = "90 days";
    public bool EnableCompression { get; init; } = true;
    public string CompressionPeriod { get; init; } = "1 day";
    public bool CreateContinuousAggregate { get; init; } = true;
}

/// <summary>
/// 하이퍼테이블 생성 결과 클래스
/// </summary>
public record HypertableCreateResult
{
    public bool Success { get; init; }
    public string Message { get; init; } = string.Empty;
    public string? TableName { get; init; }

    public static HypertableCreateResult Ok(string? tableName = null, string? message = null)
        => new() { Success = true, TableName = tableName, Message = message ?? "하이퍼테이블이 성공적으로 생성되었습니다." };

    public static HypertableCreateResult Failed(string message)
        => new() { Success = false, Message = message };

    public static HypertableCreateResult AlreadyExists(string message)
        => new() { Success = false, Message = message };
}
