using Hc900Crawler.Core.Application.DTOs;
using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using System.Text.Json;

namespace Hc900Crawler.Infrastructure.Trend;

/// <summary>
/// 트렌드 워크스페이스 서비스 — v_analog_points 조회, trend_group CRUD(멤버 JSONB), realtime tail.
/// 뷰/JSONB 매핑 편의를 위해 raw NpgsqlCommand 사용(프로젝트 QueryHistoryWithIntervalAsync 패턴 답습).
/// </summary>
public class TrendService : ITrendService
{
    private readonly Hc900DbContext _ctx;
    private readonly ILogger<TrendService> _logger;
    private static readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);

    public TrendService(Hc900DbContext ctx, ILogger<TrendService> logger)
    {
        _ctx    = ctx;
        _logger = logger;
    }

    private NpgsqlConnection NewConn() => new(_ctx.Database.GetConnectionString());

    public async Task<List<AnalogPointDto>> GetAnalogPointsAsync(string? q, CancellationToken ct)
    {
        const string sql = """
            SELECT tagname, base_tag, value, timestamp, unit, eu_lo, eu_hi, description, area
            FROM v_analog_points
            WHERE (@q = '' OR tagname ILIKE '%'||@q||'%' OR COALESCE(description,'') ILIKE '%'||@q||'%')
            ORDER BY tagname
            LIMIT 2000
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("q", q ?? "");

        var list = new List<AnalogPointDto>();
        await using var r = await cmd.ExecuteReaderAsync(ct);
        while (await r.ReadAsync(ct))
        {
            list.Add(new AnalogPointDto
            {
                TagName     = r.GetString(0),
                BaseTag     = r.IsDBNull(1) ? "" : r.GetString(1),
                Value       = r.IsDBNull(2) ? null : r.GetDouble(2),
                Timestamp   = r.GetDateTime(3),
                Unit        = r.IsDBNull(4) ? null : r.GetString(4),
                EuLo        = r.IsDBNull(5) ? null : r.GetDouble(5),
                EuHi        = r.IsDBNull(6) ? null : r.GetDouble(6),
                Description = r.IsDBNull(7) ? null : r.GetString(7),
                Area        = r.IsDBNull(8) ? null : r.GetString(8),
            });
        }
        return list;
    }

    public async Task<List<TrendGroupDto>> GetGroupsAsync(CancellationToken ct)
    {
        const string sql = "SELECT id, name, description, members, created_at, updated_at FROM trend_group ORDER BY name";
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);

        var list = new List<TrendGroupDto>();
        await using var r = await cmd.ExecuteReaderAsync(ct);
        while (await r.ReadAsync(ct)) list.Add(ReadGroup(r));
        return list;
    }

    public async Task<TrendGroupDto?> GetGroupAsync(int id, CancellationToken ct)
    {
        const string sql = "SELECT id, name, description, members, created_at, updated_at FROM trend_group WHERE id = @id";
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("id", id);

        await using var r = await cmd.ExecuteReaderAsync(ct);
        return await r.ReadAsync(ct) ? ReadGroup(r) : null;
    }

    public async Task<TrendGroupDto> CreateGroupAsync(TrendGroupUpsertDto dto, CancellationToken ct)
    {
        const string sql = """
            INSERT INTO trend_group (name, description, members)
            VALUES (@name, @desc, @members::jsonb)
            RETURNING id, name, description, members, created_at, updated_at
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("name", dto.Name);
        cmd.Parameters.AddWithValue("desc", (object?)dto.Description ?? DBNull.Value);
        cmd.Parameters.AddWithValue("members", JsonSerializer.Serialize(dto.Members ?? new(), _json));

        await using var r = await cmd.ExecuteReaderAsync(ct);
        await r.ReadAsync(ct);
        return ReadGroup(r);
    }

    public async Task<TrendGroupDto?> UpdateGroupAsync(int id, TrendGroupUpsertDto dto, CancellationToken ct)
    {
        const string sql = """
            UPDATE trend_group
            SET name = @name, description = @desc, members = @members::jsonb, updated_at = NOW()
            WHERE id = @id
            RETURNING id, name, description, members, created_at, updated_at
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("id", id);
        cmd.Parameters.AddWithValue("name", dto.Name);
        cmd.Parameters.AddWithValue("desc", (object?)dto.Description ?? DBNull.Value);
        cmd.Parameters.AddWithValue("members", JsonSerializer.Serialize(dto.Members ?? new(), _json));

        await using var r = await cmd.ExecuteReaderAsync(ct);
        return await r.ReadAsync(ct) ? ReadGroup(r) : null;
    }

    public async Task<bool> DeleteGroupAsync(int id, CancellationToken ct)
    {
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand("DELETE FROM trend_group WHERE id = @id", conn);
        cmd.Parameters.AddWithValue("id", id);
        return await cmd.ExecuteNonQueryAsync(ct) > 0;
    }

    public async Task<List<TrendLivePointDto>> GetLiveAsync(IReadOnlyList<string> tags, CancellationToken ct)
    {
        if (tags.Count == 0) return new();
        const string sql = """
            SELECT tagname, livevalue::double precision AS value, timestamp
            FROM realtime_table
            WHERE tagname = ANY(@tags) AND livevalue ~ '^-?[0-9]+(\.[0-9]+)?$'
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("tags", tags.ToArray());

        var list = new List<TrendLivePointDto>();
        await using var r = await cmd.ExecuteReaderAsync(ct);
        while (await r.ReadAsync(ct))
        {
            list.Add(new TrendLivePointDto
            {
                Tag   = r.GetString(0),
                Value = r.IsDBNull(1) ? null : r.GetDouble(1),
                Ts    = r.GetDateTime(2),
            });
        }
        return list;
    }

    public async Task<List<TrendLimitDto>> GetLimitsAsync(IReadOnlyList<string> tags, CancellationToken ct)
    {
        if (tags.Count == 0) return new();
        var bases = tags.Select(t => t.Split('.')[0]).Distinct().ToList();
        var baseArr = bases.ToArray();
        var result = new Dictionary<string, TrendLimitDto>();

        const string sql = """
            SELECT ir.base_tag, ir.unit, ir.eu_lo, ir.eu_hi,
                   rt.tagname AS sp_tag, rt.livevalue AS sp_val
            FROM v_instrument_range ir
            LEFT JOIN LATERAL (
                SELECT tagname, livevalue FROM realtime_table
                WHERE tagname = (ir.base_tag || '.sp')
                  AND livevalue ~ '^-?[0-9.]+$'
                LIMIT 1
            ) rt ON true
            WHERE ir.base_tag = ANY(@bases)
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("bases", baseArr);

        await using var r = await cmd.ExecuteReaderAsync(ct);
        while (await r.ReadAsync(ct))
        {
            var bt = r.GetString(0);
            var unit = r.IsDBNull(1) ? null : r.GetString(1);
            var euLo = r.IsDBNull(2) ? (double?)null : r.GetDouble(2);
            var euHi = r.IsDBNull(3) ? (double?)null : r.GetDouble(3);
            var spVal = r.IsDBNull(5) ? (double?)null : double.TryParse(r.GetString(5), out var sp) ? sp : null;

            foreach (var tag in tags.Where(t => t.Split('.')[0] == bt))
            {
                if (!result.ContainsKey(tag))
                {
                    result[tag] = new TrendLimitDto { Tag = tag, Unit = unit, Lo = euLo, Hi = euHi, Sp = spVal };
                }
            }
        }
        return result.Values.ToList();
    }

    public async Task<List<TrendBandDto>> GetRunBandsAsync(DateTime from, DateTime to, string? area, CancellationToken ct)
    {
        const string sql = """
            SELECT tagname, event_time, event_type,
                   LEAD(event_time) OVER (PARTITION BY tagname ORDER BY event_time) AS next_time
            FROM event_history_table
            WHERE event_time >= @from AND event_time <= @to
              AND event_type IN ('RUN', 'TRIP')
              AND (@area::text IS NULL OR area ILIKE '%'||@area::text||'%' OR sub_area ILIKE '%'||@area::text||'%')
            ORDER BY tagname, event_time
            """;
        await using var conn = NewConn();
        await conn.OpenAsync(ct);
        await using var cmd = new NpgsqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("from", from.ToUniversalTime());   // 타임존 시프트 방지
        cmd.Parameters.AddWithValue("to", to.ToUniversalTime());
        cmd.Parameters.AddWithValue("area", (object?)area ?? DBNull.Value);

        var bands = new List<TrendBandDto>();
        await using var r = await cmd.ExecuteReaderAsync(ct);
        while (await r.ReadAsync(ct))
        {
            var tag = r.GetString(0);
            var t0 = r.GetDateTime(1);
            var type = r.GetString(2);
            var t1 = r.IsDBNull(3) ? to : r.GetDateTime(3);
            if (t1 > to) t1 = to;
            bands.Add(new TrendBandDto { Tag = tag, T0 = t0, T1 = t1, State = type });
        }
        return bands;
    }

    private static TrendGroupDto ReadGroup(NpgsqlDataReader r)
    {
        var membersJson = r.IsDBNull(3) ? "[]" : r.GetFieldValue<string>(3);
        List<TrendMemberDto> members;
        try { members = JsonSerializer.Deserialize<List<TrendMemberDto>>(membersJson, _json) ?? new(); }
        catch { members = new(); }

        return new TrendGroupDto
        {
            Id          = r.GetInt32(0),
            Name        = r.GetString(1),
            Description = r.IsDBNull(2) ? null : r.GetString(2),
            Members     = members,
            CreatedAt   = r.GetDateTime(4),
            UpdatedAt   = r.GetDateTime(5),
        };
    }
}
