using System.Text.RegularExpressions;
using Hc900Crawler.Core.Application.DTOs;

namespace Hc900Crawler.Core.Application.Services;

/// <summary>
/// LLM이 생성한 SQL을 PostgreSQL 실행 전에 다단계 검증.
/// SELECT 전용, 허용된 테이블/함수만 허용.
/// </summary>
public class SqlValidator
{
    // ── 설정 ────────────────────────────────────────────────────────
    private readonly SqlValidatorOptions _opts;

    public SqlValidator(SqlValidatorOptions? opts = null)
        => _opts = opts ?? SqlValidatorOptions.Default;

    // ════════════════════════════════════════════════════════════════
    // Public Entry Point
    // ════════════════════════════════════════════════════════════════
    public ValidationResult Validate(string sql)
    {
        if (string.IsNullOrWhiteSpace(sql))
            return ValidationResult.Fail(
                ValidationFailReason.EmptyInput, "SQL이 비어 있습니다.");

        var normalized = Normalize(sql);

        return CheckSelectOnly(normalized)
            ?? CheckDangerousKeywords(normalized)
            ?? CheckForbiddenClauses(normalized)
            ?? CheckDisallowedFunctions(normalized)
            ?? CheckTableReferences(normalized)
            ?? CheckSubqueryDepth(normalized)
            ?? CheckSuspiciousPatterns(normalized)
            ?? ValidationResult.Ok();
    }

    // ════════════════════════════════════════════════════════════════
    // ① SELECT 전용 검사
    // ════════════════════════════════════════════════════════════════
    private static ValidationResult? CheckSelectOnly(string sql)
    {
        // 첫 번째 유효 토큰이 SELECT 이어야 함
        var firstToken = sql.TrimStart().Split(
            new char[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
            .FirstOrDefault()?.ToUpperInvariant();

        if (firstToken != "SELECT")
            return ValidationResult.Fail(
                ValidationFailReason.NotSelectStatement,
                $"SELECT로 시작해야 합니다. 감지된 시작: '{firstToken}'");

        return null;
    }

    // ════════════════════════════════════════════════════════════════
    // ② 위험 키워드 검사 (단어 경계 기준)
    // ════════════════════════════════════════════════════════════════
    private static readonly string[] DangerousKeywords =
    [
        // DDL
        "DROP", "CREATE", "ALTER", "TRUNCATE", "RENAME",
        // DML
        "INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT",
        // 권한
        "GRANT", "REVOKE",
        // 트랜잭션 제어
        "COMMIT", "ROLLBACK", "SAVEPOINT",
        // 시스템
        "COPY", "VACUUM", "ANALYZE", "REINDEX", "CLUSTER",
        // 확장
        "CREATE EXTENSION", "LOAD",
        // 파일 접근
        "PG_READ_FILE", "PG_WRITE_FILE", "PG_READ_BINARY_FILE",
    ];

    private static ValidationResult? CheckDangerousKeywords(string sql)
    {
        foreach (var kw in DangerousKeywords)
        {
            // 단어 경계 검사: DROP TABLE 처럼 앞뒤가 비-알파벳
            var pattern = $@"(?<![A-Z_])({Regex.Escape(kw)})(?![A-Z_])";
            if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase))
                return ValidationResult.Fail(
                    ValidationFailReason.DangerousKeyword,
                    $"허용되지 않는 키워드가 포함되어 있습니다: '{kw}'");
        }
        return null;
    }

    // ════════════════════════════════════════════════════════════════
    // ③ 허용되지 않는 절(Clause) 검사
    // ════════════════════════════════════════════════════════════════
    private static readonly string[] ForbiddenClauses =
    [
        // 스토어드 프로시저 / 함수 실행
        @"CALL\s+\w",
        @"EXECUTE\s+\w",
        @"PERFORM\s+\w",
        // INTO (SELECT INTO = 테이블 생성)
        @"SELECT\s+.+\s+INTO\s+\w",
        // 다중 구문 (세미콜론 뒤 추가 구문)
        @";\s*\w",
        // 주석을 이용한 우회 시도
        @"/\*.*?\*/",           // 블록 주석
        @"--[^\n]*\n.*SELECT",  // 인라인 주석 뒤 SELECT (우회 패턴)
        // pg_* 시스템 함수 직접 호출
        @"pg_sleep\s*\(",
        @"pg_cancel_backend\s*\(",
        @"pg_terminate_backend\s*\(",
    ];

    private static ValidationResult? CheckForbiddenClauses(string sql)
    {
        foreach (var pattern in ForbiddenClauses)
        {
            if (Regex.IsMatch(sql, pattern,
                RegexOptions.IgnoreCase | RegexOptions.Singleline))
                return ValidationResult.Fail(
                    ValidationFailReason.ForbiddenClause,
                    $"허용되지 않는 구문 패턴이 감지되었습니다: '{pattern}'");
        }
        return null;
    }

    // ════════════════════════════════════════════════════════════════
    // ④ 허용 함수 화이트리스트
    // ════════════════════════════════════════════════════════════════
    // 집계/분석/시간 관련 함수만 허용
    private static readonly HashSet<string> AllowedFunctions = new(StringComparer.OrdinalIgnoreCase)
    {
        // 집계
        "COUNT", "SUM", "AVG", "MIN", "MAX", "STDDEV", "STDDEV_POP", "STDDEV_SAMP",
        "VARIANCE", "VAR_POP", "VAR_SAMP",
        // 통계 회귀
        "REGR_SLOPE", "REGR_INTERCEPT", "REGR_R2", "REGR_COUNT",
        "REGR_AVGX", "REGR_AVGY", "REGR_SXX", "REGR_SYY", "REGR_SXY",
        // 윈도우
        "ROW_NUMBER", "RANK", "DENSE_RANK", "NTILE",
        "LAG", "LEAD", "FIRST_VALUE", "LAST_VALUE", "NTH_VALUE",
        // 시간
        "NOW", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME",
        "DATE_TRUNC", "DATE_PART", "EXTRACT", "AGE",
        "TO_TIMESTAMP", "TO_CHAR",
        // TimescaleDB (first/last는 시계열 전용 함수)
        "TIME_BUCKET", "TIME_BUCKET_GAPFILL",
        "LOCF", "INTERPOLATE",
        "FIRST", "LAST",
        // 문자열 (tagname 처리용)
        "UPPER", "LOWER", "TRIM", "LTRIM", "RTRIM",
        "SUBSTRING", "SPLIT_PART", "REGEXP_REPLACE",
        // 타입 변환
        "CAST", "COALESCE", "NULLIF", "GREATEST", "LEAST",
        // 수학
        "ABS", "ROUND", "CEIL", "FLOOR", "POWER", "SQRT", "LN", "LOG",
    };

    // 함수 호출 패턴: 단어(공백*)(  형태
    private static readonly Regex FuncCallPattern =
        new(@"\b([A-Z_][A-Z0-9_]*)\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    private static ValidationResult? CheckDisallowedFunctions(string sql)
    {
        // 문자열 리터럴 내부는 제외 (작은따옴표 사이)
        var stripped = Regex.Replace(sql, @"'[^']*'", "''");

        foreach (Match m in FuncCallPattern.Matches(stripped))
        {
            var fn = m.Groups[1].Value.ToUpperInvariant();

            // SQL 키워드는 함수가 아님 (CASE WHEN, OVER, FILTER 등)
            if (SqlKeywordExceptions.Contains(fn)) continue;

            if (!AllowedFunctions.Contains(fn))
                return ValidationResult.Fail(
                    ValidationFailReason.DisallowedFunction,
                    $"허용되지 않는 함수입니다: '{fn}()'");
        }
        return null;
    }

    private static readonly HashSet<string> SqlKeywordExceptions = new(StringComparer.OrdinalIgnoreCase)
    {
        "OVER", "FILTER", "WITHIN", "CASE", "WHEN", "THEN", "ELSE", "END",
        "IN", "NOT", "AND", "OR", "AS", "ON", "AT", "BY",
        "FROM", "WHERE", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET",
        "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "CROSS", "NATURAL",
        "UNION", "INTERSECT", "EXCEPT",
        "VALUES", "SET", "RETURNING",
    };

    // ════════════════════════════════════════════════════════════════
    // ⑤ 테이블 참조 검사 (허용 테이블 화이트리스트)
    // ════════════════════════════════════════════════════════════════
    private ValidationResult? CheckTableReferences(string sql)
    {
        // FROM / JOIN 뒤에 오는 테이블명 추출
        var tablePattern = new Regex(
            @"(?:FROM|JOIN)\s+([A-Z_][A-Z0-9_.]*)",
            RegexOptions.IgnoreCase);

        // 필수 테이블 포함 여부
        var tables = tablePattern.Matches(sql)
            .Select(m => m.Groups[1].Value.ToLowerInvariant())
            .ToList();

        if (_opts.RequiredTables.Any() &&
            !tables.Any(t => _opts.RequiredTables.Contains(t)))
            return ValidationResult.Fail(
                ValidationFailReason.MissingRequiredTable,
                $"필수 테이블이 없습니다. 필요: [{string.Join(", ", _opts.RequiredTables)}]");

        // 허용 테이블 외 참조 차단
        foreach (var table in tables)
        {
            // 서브쿼리 별칭은 소문자 단어 — 허용 목록에 없어도 통과
            if (_opts.AllowedTables.Any() &&
                !_opts.AllowedTables.Contains(table) &&
                !IsLikelySubqueryAlias(table))
                return ValidationResult.Fail(
                    ValidationFailReason.UnsafeTableReference,
                    $"허용되지 않는 테이블 참조: '{table}'");
        }
        return null;
    }

    // 서브쿼리 별칭: 2~20자 소문자 단어
    private static bool IsLikelySubqueryAlias(string name)
        => Regex.IsMatch(name, @"^[a-z][a-z0-9_]{1,19}$");

    // ════════════════════════════════════════════════════════════════
    // ⑥ 서브쿼리 깊이 검사
    // ════════════════════════════════════════════════════════════════
    private ValidationResult? CheckSubqueryDepth(string sql)
    {
        int depth = 0, maxDepth = 0;
        foreach (var c in sql)
        {
            if (c == '(') { depth++; maxDepth = Math.Max(maxDepth, depth); }
            else if (c == ')') depth--;
        }

        if (maxDepth > _opts.MaxSubqueryDepth)
            return ValidationResult.Fail(
                ValidationFailReason.SubqueryDepthExceeded,
                $"서브쿼리 깊이 초과: {maxDepth} > 허용 {_opts.MaxSubqueryDepth}");

        return null;
    }

    // ════════════════════════════════════════════════════════════════
    // ⑦ 의심 패턴 검사 (SQL Injection 우회 시도)
    // ════════════════════════════════════════════════════════════════
    private static readonly (string Pattern, string Desc)[] SuspiciousPatterns =
    [
        (@"'\s*OR\s*'?\d",         "OR 기반 Injection 패턴"),
        (@"'\s*;\s*--",            "주석 종료 Injection"),
        (@"UNION\s+ALL\s+SELECT",  "UNION 기반 Injection"),
        (@"UNION\s+SELECT",        "UNION 기반 Injection"),
        (@"0x[0-9A-F]{4,}",        "16진수 인코딩 우회"),
        (@"CHAR\s*\(\s*\d",        "CHAR() 인코딩 우회"),
        (@"WAITFOR\s+DELAY",       "시간 지연 공격"),
        (@"BENCHMARK\s*\(",        "벤치마크 기반 공격"),
        (@"\bINFORMATION_SCHEMA\b","시스템 스키마 접근"),
        (@"\bPG_CATALOG\b",        "시스템 카탈로그 접근"),
        (@"\bPG_STAT\b",           "통계 뷰 접근"),
    ];

    private static ValidationResult? CheckSuspiciousPatterns(string sql)
    {
        foreach (var (pattern, desc) in SuspiciousPatterns)
        {
            if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase))
                return ValidationResult.Fail(
                    ValidationFailReason.SuspiciousPattern,
                    $"의심스러운 패턴 감지: {desc}");
        }
        return null;
    }

    // ════════════════════════════════════════════════════════════════
    // 공통 정규화
    // ════════════════════════════════════════════════════════════════
    private static string Normalize(string sql)
        => Regex.Replace(sql.Trim().TrimEnd(';'), @"\s+", " ");
}
