using System.Text.RegularExpressions;
using Hc900Crawler.Core.Application.DTOs;
using Hc900Crawler.Core.Application.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Npgsql;

namespace Hc900Crawler.Core.Application.Services;

/// <summary>
/// Text-to-SQL 시계열 쿼리 서비스
/// 자연어 질의를 파싱하여 iiot-timescaledb(IIoT 플랫폼) SQL 쿼리를 생성하고 실행합니다.
/// history_table (recorded_at: TIMESTAMPTZ, tagname: TEXT, value: TEXT) + node_map_master 테이블을 참조합니다.
/// </summary>
public class TextToSqlService : ITextToSqlService
{
    private readonly ILogger<TextToSqlService> _logger;
    private readonly IConfiguration _configuration;
    private readonly string _connectionString;
    private readonly KstClock _kstClock;
    private readonly KoreanTimeRangeExtractor _timeExtractor;
    private readonly SqlValidator _validator;

    public TextToSqlService(
        ILogger<TextToSqlService> logger,
        IConfiguration configuration,
        KstClock? kstClock = null,
        KoreanTimeRangeExtractor? timeExtractor = null,
        SqlValidator? validator = null)
    {
        _logger = logger;
        _configuration = configuration;
        _connectionString = configuration.GetConnectionString("DefaultConnection")
            ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
        _kstClock = kstClock ?? new KstClock(new SystemClock());
        _timeExtractor = timeExtractor ?? new KoreanTimeRangeExtractor(_kstClock);
        _validator = validator ?? new SqlValidator();
    }

    // ── 테이블명 설정 (iiot-timescaledb 스키마) ─────────────────────────────────
    // history_table: node_id(TEXT), recorded_at(TIMESTAMPTZ), tagname(TEXT), value(TEXT)
    // node_map_master: level, class, name, node_id, data_type
    private string HistoryTable => "history_table";
    private string MasterTable  => "node_map_master";

    // ── 자연어 파싱 ─────────────────────────────────────────────────────────────

    /// <summary>
    /// 자연어 입력을 파싱하여 SQL 쿼리로 변환합니다.
    /// 최대 8개의 태그명을 지원합니다.
    /// </summary>
    public async Task<string> ParseNaturalLanguageAsync(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            throw new ArgumentException("쿼리 입력이 필요합니다.", nameof(input));

        var (sqls, tagNames) = BuildSqlFromNaturalLanguage(input);
        var sql = sqls.FirstOrDefault() ?? string.Empty;

        // 태그명이 추출되지 않으면 예외 발생
        if (tagNames.Count == 0)
        {
            var lower = input.ToLower();
            var hasTimeKeyword = timeKeywords.Any(kw => lower.Contains(kw));

            if (hasTimeKeyword)
                throw new ArgumentException("시간 키워드만 입력되었습니다. 태그명을 지정해야 합니다.", nameof(input));
            throw new ArgumentException("태그명을 지정해야 합니다.", nameof(input));
        }
        
        _logger.LogInformation("[TextToSql] 자연어 파싱: \"{Input}\" → {Sql}", input, sql);
        return await Task.FromResult(sql);
    }
    
    /// <summary>
    /// 입력이 시간 키워드만 포함하는지 확인
    /// </summary>
    private bool IsTimeKeywordOnly(string input)
    {
        var lower = input.ToLower();
        var hasTimeKeyword = timeKeywords.Any(kw => lower.Contains(kw));
        var hasTagName = ContainsTagName(input);
        return hasTimeKeyword && !hasTagName;
    }
    
    /// <summary>
    /// 자연어 입력에서 태그명들을 추출합니다.
    /// </summary>
    private List<string> ExtractTagNames(string input)
    {
        var tagNames = new List<string>();
        var tagPattern = new Regex(@"[A-Za-z0-9][A-Za-z0-9._\-=/:@]*[A-Za-z0-9]");
        var matches = tagPattern.Matches(input);

        foreach (Match match in matches.Cast<Match>().Take(8))
        {
            string potentialTag = match.Value;
            
            // 너무 짧은 단어나 단순 숫자는 제외
            if (potentialTag.Length < 2) continue;

            // 시간 키워드, 집계 키워드, 순수 숫자 제외
            if (timeKeywords.Any(kw => potentialTag.Equals(kw, StringComparison.OrdinalIgnoreCase))) continue;
            if (aggregateKeywords.Any(kw => potentialTag.Equals(kw, StringComparison.OrdinalIgnoreCase))) continue;
            if (System.Text.RegularExpressions.Regex.IsMatch(potentialTag, @"^\d+$")) continue;

            // 추가 필터링: 'average', 'avg', 'max', 'min' 등 일반적인 영어 단어 제외
            var commonWords = new[] { "average", "avg", "max", "min", "first", "last", "result", "bucket", "tagname", "value", "data" };
            if (commonWords.Contains(potentialTag.ToLower())) continue;

            // 한글이 포함되어 있다면 태그로 간주하지 않음 (태그명은 영문/숫자 기반)
            if (System.Text.RegularExpressions.Regex.IsMatch(potentialTag, @"[\uAC00-\uD7AF]"))
                continue;
            
            tagNames.Add(potentialTag);
        }
        return tagNames;
    }
    private readonly string[] timeKeywords = new[] { "최근", "오늘", "어제", "오늘부터", "어제부터", "최근의", "오늘의", "어제의", "시간", "분", "일", "개월", "년", "현재" };
    private readonly string[] aggregateKeywords = new[] { "평균", "avg", "max", "최대", "min", "최소", "first", "첫", "last", "마지막" };

    /// <summary>
    /// 입력이 태그명을 포함하는지 확인
    /// </summary>
    private bool ContainsTagName(string input)
    {
        // OPC UA node_id 형식 확인
        if (System.Text.RegularExpressions.Regex.IsMatch(input, @"ns=\d+;s=[^\s,]+"))
            return true;

        // 태그명 패턴: 알파벳+숫자 조합, 하이픈/점/언더스코어 포함
        // 단, 순수하게 숫자만 있거나 너무 짧은 것은 제외
        var tagPattern = @"[A-Za-z0-9][A-Za-z0-9._\-=/:@]*[A-Za-z0-9]";
        var matches = System.Text.RegularExpressions.Regex.Matches(input, tagPattern);
        
        foreach (System.Text.RegularExpressions.Match match in matches)
        {
            string val = match.Value;
            if (val.Length < 2) continue;
            if (timeKeywords.Any(kw => val.Equals(kw, StringComparison.OrdinalIgnoreCase))) continue;
            if (aggregateKeywords.Any(kw => val.Equals(kw, StringComparison.OrdinalIgnoreCase))) continue;
            if (System.Text.RegularExpressions.Regex.IsMatch(val, @"^\d+$")) continue; // 순수 숫자 제외
            
            return true;
        }

        return false;
    }

    /// <summary>
    /// 자연어에서 집계 함수 추출
    /// </summary>
    private string ExtractAggregate(string lower)
    {
        if (lower.Contains("최대") || lower.Contains("max")) return "max";
        if (lower.Contains("최소") || lower.Contains("min")) return "min";
        if (lower.Contains("첫") || lower.Contains("first")) return "first";
        if (lower.Contains("마지막") || lower.Contains("last")) return "last";
        if (lower.Contains("평균") || lower.Contains("avg")) return "avg";
        return "last";
    }

    /// <summary>
    /// 자연어 입력에서 태그명, 시간 범위, 집계 함수를 추출하여 SQL 생성
    /// history_table: recorded_at (TIMESTAMPTZ), tagname (TEXT), value (TEXT)
    /// </summary>
    private (List<string> sqls, List<string> tagNames) BuildSqlFromNaturalLanguage(string input)
    {
        var lower = input.ToLower();

        // 태그명 추출 (여러 태그명 지원, 쉼표로 구분)
        var tagNames = ExtractTagNames(input);

        // 시간 범위 추출 (새로운 KoreanTimeRangeExtractor 사용)
        var timeRange = _timeExtractor.Extract(input);
        var timeCondition = timeRange.ToSqlCondition("recorded_at", _kstClock);

        // 집계 함수 추출
        var aggregate = ExtractAggregate(lower);

        // 시간대별 집계 간격 결정 (상대 범위인 경우와 절대 범위인 경우 구분)
        var timeBucket = timeRange.PostgresInterval != null ? DetermineTimeBucketFromInterval(timeRange.PostgresInterval) : "5 min";

        // history_table 조회 SQL 생성
        if (tagNames.Count > 0)
        {
            // SQL 인젝션 방지를 위해 태그명 이스케이프 및 대문자 변환
            var escapedTagNames = tagNames.Select(t => t.ToUpper().Replace("'", "''")).ToList();
            var tagNameList = string.Join(", ", escapedTagNames.Select(t => $"'{t}'"));
            var whereTag = $"WHERE tagname IN ({tagNameList}) AND {timeCondition}";

            if (aggregate != "last" && aggregate != "first")
            {
                // 집계 쿼리 (value는 TEXT 타입이므로 double precision으로 CAST)
                return (new List<string> {
                    $"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " +
                    $"tagname, {aggregate}(value::double precision) AS result " +
                    $"FROM {HistoryTable} " +
                    $"{whereTag} " +
                    $"GROUP BY 1, 2 ORDER BY 1, 2"
                }, tagNames);
            }
            else
            {
                // first/last 쿼리 (TimeScaleDB 함수)
                var func = aggregate == "first" ? "first" : "last";
                return (new List<string> {
                    $"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " +
                    $"tagname, {func}(value::double precision, recorded_at) AS result " +
                    $"FROM {HistoryTable} " +
                    $"{whereTag} " +
                    $"GROUP BY 1, 2 ORDER BY 1, 2"
                }, tagNames);
            }
        }

        // 태그명 없이 전체 조회
        return (new List<string>
        {
            $"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " +
            $"tagname, {aggregate}(value::double precision) AS result " +
            $"FROM {HistoryTable} " +
            $"WHERE {timeCondition} " +
            $"GROUP BY 1, 2 ORDER BY 1, 2"
        }, new List<string>());

        }

    /// <summary>
    /// INTERVAL 문자열에서 적절한 시간 집계 간격 결정 (date_trunc용)
    /// </summary>
    private string DetermineTimeBucketFromInterval(string interval)
    {
        return interval switch
        {
            "1 minute" or "1 min" or "1m" => "minute",
            "5 minutes" or "5 min" or "5m" => "minute",
            "10 minutes" or "10 min" or "10m" => "minute",
            "1 hour" or "1 hours" or "1h" => "hour",
            "2 hours" or "2h" => "hour",
            "3 hours" or "3h" => "hour",
            "1 day" or "1 days" or "1d" => "day",
            "2 days" or "2d" => "day",
            "3 days" or "3d" => "day",
            "7 days" or "7d" => "day",
            "14 days" or "14d" => "day",
            "30 days" or "30d" => "hour",
            _ => "minute"
        };
    }

    // ── SQL 실행 ────────────────────────────────────────────────────────────────

    public async Task<SqlQueryResultDto> ExecuteQueryAsync(string sql, int? limit = 1000)
    {
        // ── SQL 검증 (다단계 검증기) ─────────────────────────────────────────────
        if (string.IsNullOrWhiteSpace(sql))
        {
            _logger.LogWarning("[TextToSql] SQL이 비어있음");
            throw new ArgumentException("SQL이 비어있음", nameof(sql));
        }
        
        if (sql == null)
        {
            _logger.LogWarning("[TextToSql] SQL이 null임");
            throw new ArgumentNullException(nameof(sql));
        }
        
        var validationResult = _validator.Validate(sql);
        if (!validationResult.IsValid)
        {
            _logger.LogWarning("[TextToSql] SQL 검증 실패: {Reason} - {Message}", validationResult.Reason, validationResult.Message);
            return new SqlQueryResultDto
            {
                Success = false,
                Error = $"SQL 검증 실패: {validationResult.Message}"
            };
        }

        try
        {
            using var conn = new NpgsqlConnection(_connectionString);
            await conn.OpenAsync();

            var fullSql = sql.TrimEnd().TrimEnd(';');
            if (limit.HasValue)
            {
                // SQL에 이미 LIMIT가 포함되어 있으면 추가하지 않음
                var hasLimit = false;
                var lowerSql = fullSql.ToLowerInvariant();
                // "limit" 키워드 검색 (단어 경계 확인)
                var index = 0;
                while ((index = lowerSql.IndexOf("limit", index)) != -1)
                {
                    var endIdx = index + 5;
                    // "limit" 다음 문자가 공백, 세미콜론, 또는 문자열 끝이면 LIMIT 키워드
                    if (endIdx >= lowerSql.Length || char.IsWhiteSpace(lowerSql[endIdx]) || lowerSql[endIdx] == ';')
                    {
                        // "select", "from", "where", "order", "group", "having" 등의 일부가 아닌지 확인
                        var startIdx = index - 1;
                        var isPartOfWord = false;
                        while (startIdx >= 0 && (char.IsLetterOrDigit(lowerSql[startIdx]) || lowerSql[startIdx] == '_'))
                        {
                            isPartOfWord = true;
                            startIdx--;
                        }
                        if (!isPartOfWord)
                        {
                            hasLimit = true;
                        }
                        break;
                    }
                    index++;
                }
                if (!hasLimit)
                {
                    fullSql += $" LIMIT {limit.Value}";
                }
            }

            // ── 태그 존재 여부 확인 ─────────────────────────────────────────────────
            // SQL에서 태그명 추출하여 history_table에 존재하는지 확인
            var tagName = ExtractTagNameFromSql(fullSql);
            if (!string.IsNullOrEmpty(tagName))
            {
                var tagExists = await CheckTagExistsAsync(conn, tagName);
                if (!tagExists)
                {
                    _logger.LogInformation("[TextToSql] 태그 '{TagName}'이(가) history_table에 존재하지 않음", tagName);
                    return new SqlQueryResultDto
                    {
                        Success = false, // Success를 false로 변경하여 에러로 처리
                        Error = $"태그 '{tagName}'이(가) 데이터베이스에 존재하지 않습니다.",
                        Columns = new List<string> { "bucket", "tagname", "result" },
                        Rows = new List<Dictionary<string, object?>>(),
                        TotalCount = 0
                    };
                }
            }

            using var cmd = new NpgsqlCommand(fullSql, conn);
            using var reader = await cmd.ExecuteReaderAsync();

            var columns = new List<string>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                columns.Add(reader.GetName(i));
            }

            var rows = new List<Dictionary<string, object?>>();
            while (await reader.ReadAsync())
            {
                var row = new Dictionary<string, object?>();
                for (int i = 0; i < reader.FieldCount; i++)
                {
                    row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
                }
                rows.Add(row);
            }

            _logger.LogInformation("[TextToSql] 쿼리 실행 성공: {RowCount}행", rows.Count);
            return new SqlQueryResultDto
            {
                Success = true,
                Message = rows.Count == 0 ? "조회 결과가 없습니다." : null,
                Columns = columns,
                Rows = rows,
                TotalCount = rows.Count
            };
        }
        catch (NpgsqlException npgsqlEx)
        {
            // PostgreSQL 특정 오류
            _logger.LogError("[TextToSql] PostgreSQL 오류 (코드: {ErrorCode}): {ErrorMessage}\nSQL: {Sql}",
                npgsqlEx.SqlState, npgsqlEx.Message, sql);
            return new SqlQueryResultDto
            {
                Success = false,
                Error = $"PostgreSQL 오류 [{npgsqlEx.SqlState}]: {npgsqlEx.Message}"
            };
        }
        catch (Exception ex)
        {
            // 일반 예외 - 내부 예외 정보 포함
            var innerExceptionMsg = ex.InnerException?.Message ?? string.Empty;
            var fullMessage = string.IsNullOrEmpty(innerExceptionMsg)
                ? ex.Message
                : $"{ex.Message} (내부: {innerExceptionMsg})";
            
            _logger.LogError(ex, "[TextToSql] 쿼리 실행 실패: {Sql}\n오류: {Message}", sql, fullMessage);
            return new SqlQueryResultDto
            {
                Success = false,
                Error = fullMessage
            };
        }
    }

    /// <summary>
    /// SQL에서 태그명을 추출합니다.
    /// 예: "WHERE tagname IN ('p-61902.hzset.fieldvalue')" → "p-61902.hzset.fieldvalue"
    /// </summary>
    private string? ExtractTagNameFromSql(string sql)
    {
        // tagname IN (...) 패턴 추출
        var inPattern = new System.Text.RegularExpressions.Regex(
            @"tagname\s*IN\s*\(\s*'([^']+)'\s*\)",
            System.Text.RegularExpressions.RegexOptions.IgnoreCase);
        var match = inPattern.Match(sql);
        if (match.Success)
        {
            return match.Groups[1].Value;
        }

        // tagname = '...' 패턴 추출
        var eqPattern = new System.Text.RegularExpressions.Regex(
            @"tagname\s*=\s*'([^']+)'\s*",
            System.Text.RegularExpressions.RegexOptions.IgnoreCase);
        match = eqPattern.Match(sql);
        if (match.Success)
        {
            return match.Groups[1].Value;
        }

        return null;
    }

    /// <summary>
    /// 태그가 history_table에 존재하는지 확인합니다.
    /// </summary>
    private async Task<bool> CheckTagExistsAsync(NpgsqlConnection conn, string tagName)
    {
        try
        {
            using var cmd = new NpgsqlCommand(
                "SELECT 1 FROM history_table WHERE tagname = @tagName LIMIT 1", conn);
            cmd.Parameters.Add(new NpgsqlParameter("@tagName", tagName));
            var result = await cmd.ExecuteScalarAsync();
            return result != null;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[TextToSql] 태그 존재 확인 실패: {TagName}", tagName);
            // 확인 실패 시 false 반환 (보안상 태그가 존재하지 않는 것으로 간주)
            return false;
        }
    }

    // ── 쿼리 제안 ───────────────────────────────────────────────────────────────

    public Task<IEnumerable<string>> SuggestQueriesAsync(string partialInput)
    {
        var suggestions = new List<string>
        {
            "최근 1시간 평균",
            "최근 24시간 최대값",
            "최근 7일 최소값",
            "전체 태그 현재 값",
            "오늘의 데이터 추세"
        };

        if (string.IsNullOrWhiteSpace(partialInput))
            return Task.FromResult<IEnumerable<string>>(suggestions);

        var lower = partialInput.ToLower();
        return Task.FromResult<IEnumerable<string>>(suggestions.Where(s => s.Contains(lower)));
    }

    // ── 시계열 분석 ─────────────────────────────────────────────────────────────

    public async Task<TimeSeriesAnalysisDto> AnalyzeAsync(AnalyzeRequestDto dto)
    {
        var result = new TimeSeriesAnalysisDto();

        try
        {
            using var conn = new NpgsqlConnection(_connectionString);
            await conn.OpenAsync();

            var tagNames = dto.TagNames.Count > 0
                ? dto.TagNames
                : await GetAllTagNamesAsync(conn);

            // 날짜 파라미터도 SQL 인젝션 방지를 위해 parameterized 처리
            var fromTimestamp = dto.From ?? DateTime.Now.AddDays(-1);
            var toTimestamp = dto.To ?? DateTime.Now;
            
            var tagResults = new List<AnalysisTagResult>();

            foreach (var tagName in tagNames)
            {
                // SQL 인젝션 방지를 위해 parameterized query 사용
                var sql = @"
                    SELECT
                        AVG(value::double precision) AS avg_val,
                        MIN(value::double precision) AS min_val,
                        MAX(value::double precision) AS max_val,
                        STDDEV(value::double precision) AS stddev_val,
                        first(value::double precision, recorded_at) AS first_val,
                        last(value::double precision, recorded_at) AS last_val,
                        COUNT(*) AS point_count
                    FROM history_table
                    WHERE tagname = @tagName
                      AND recorded_at BETWEEN @fromTimestamp AND @toTimestamp";

                using var cmd = new NpgsqlCommand(sql, conn);
                cmd.Parameters.AddWithValue("@tagName", tagName);
                cmd.Parameters.AddWithValue("@fromTimestamp", fromTimestamp);
                cmd.Parameters.AddWithValue("@toTimestamp", toTimestamp);
                
                using var reader = await cmd.ExecuteReaderAsync();

                if (await reader.ReadAsync())
                {
                    tagResults.Add(new AnalysisTagResult
                    {
                        TagName = tagName,
                        Avg = reader.IsDBNull(0) ? null : Convert.ToDouble(reader.GetValue(0)),
                        Min = reader.IsDBNull(1) ? null : Convert.ToDouble(reader.GetValue(1)),
                        Max = reader.IsDBNull(2) ? null : Convert.ToDouble(reader.GetValue(2)),
                        StdDev = reader.IsDBNull(3) ? null : Convert.ToDouble(reader.GetValue(3)),
                        First = reader.IsDBNull(4) ? null : Convert.ToDouble(reader.GetValue(4)),
                        Last = reader.IsDBNull(5) ? null : Convert.ToDouble(reader.GetValue(5)),
                        PointCount = reader.IsDBNull(6) ? 0 : Convert.ToInt64(reader.GetValue(6)),
                        From = dto.From,
                        To = dto.To
                    });
                }
            }

            result.Tags = tagResults;
            result.Success = true;
            _logger.LogInformation("[TextToSql] 분석 완료: {TagCount}개 태그", tagResults.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[TextToSql] 분석 실패");
            result.Success = false;
            result.Error = ex.Message;
        }

        return result;
    }

    private async Task<List<string>> GetAllTagNamesAsync(NpgsqlConnection conn)
    {
        var tags = new List<string>();
        try
        {
            // node_map_master에서 고유 name 목록 조회 (Experion 태그명)
            using var cmd = new NpgsqlCommand(
                "SELECT DISTINCT name FROM node_map_master ORDER BY name", conn);
            using var reader = await cmd.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                tags.Add(reader.GetString(0));
            }
        }
        catch
        {
            // fallback: empty list
        }
        return tags;
    }
}
