// The MIT License (MIT)
//
// Copyright (c) 2015-2025 Rasmus Mikkelsen
// https://github.com/eventflow/EventFlow
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using EventFlow.Extensions;
using EventFlow.ReadStores;
using EventFlow.Sql.ReadModels;
using EventFlow.Sql.ReadModels.Attributes;

namespace Books.Api.EventFlow.Customs;

/// <summary>
/// Custom SQL generator for PostgreSQL that uses snake_case column names
/// while keeping PascalCase property names for Dapper parameter binding.
/// </summary>
public class ReadModelSqlGenerator : IReadModelSqlGenerator
{
    private static readonly ConcurrentDictionary<Type, string> TableNames = new();
    private static readonly ConcurrentDictionary<Type, IReadOnlyCollection<PropertyInfo>> PropertyInfos = new();
    private static readonly ConcurrentDictionary<Type, string> IdentityColumns = new();
    private static readonly ConcurrentDictionary<Type, string> IdentityProperties = new();
    private static readonly ConcurrentDictionary<Type, string> VersionColumns = new();

    private readonly ConcurrentDictionary<Type, string?> _insertSqls = new();
    private readonly ConcurrentDictionary<Type, string> _purgeSqls = new();
    private readonly ConcurrentDictionary<Type, string> _deleteSqls = new();
    private readonly ConcurrentDictionary<Type, string> _selectSqls = new();
    private readonly ConcurrentDictionary<Type, string> _updateSqls = new();

    public string? CreateInsertSql<TReadModel>() where TReadModel : IReadModel
    {
        var readModelType = typeof(TReadModel);
        if (_insertSqls.TryGetValue(readModelType, out var sql))
        {
            return sql;
        }

        var properties = GetPropertyInfos(readModelType);

        // Column names in snake_case
        var columnList = string.Join(", ", properties.Select(p => p.Name.ToSnakeCase()));

        // Parameter names use property names (PascalCase) for Dapper binding
        var parameterList = string.Join(", ", properties.Select(p => $"@{p.Name}"));

        sql = $"INSERT INTO {GetTableName<TReadModel>()} ({columnList}) VALUES ({parameterList})";

        _insertSqls[readModelType] = sql;

        return sql;
    }

    public string CreateSelectSql<TReadModel>() where TReadModel : IReadModel
    {
        var readModelType = typeof(TReadModel);
        if (_selectSqls.TryGetValue(readModelType, out var sql))
        {
            return sql;
        }

        var tableName = GetTableName<TReadModel>();
        var identityColumn = GetIdentityColumn<TReadModel>();
        var properties = GetPropertyInfos(readModelType);

        // Use explicit column aliases to map snake_case columns to PascalCase properties
        var columnList = string.Join(", ", properties.Select(p => $"{p.Name.ToSnakeCase()} AS \"{p.Name}\""));

        sql = $"SELECT {columnList} FROM {tableName} WHERE {identityColumn} = @EventFlowReadModelId";

        _selectSqls[readModelType] = sql;

        return sql;
    }

    public string CreateDeleteSql<TReadModel>() where TReadModel : IReadModel
    {
        var readModelType = typeof(TReadModel);
        if (_deleteSqls.TryGetValue(readModelType, out var sql))
        {
            return sql;
        }

        sql = $"DELETE FROM {GetTableName<TReadModel>()} WHERE {GetIdentityColumn<TReadModel>()} = @EventFlowReadModelId";
        _deleteSqls[readModelType] = sql;

        return sql;
    }

    public string CreateUpdateSql<TReadModel>() where TReadModel : IReadModel
    {
        var readModelType = typeof(TReadModel);
        if (_updateSqls.TryGetValue(readModelType, out var sql))
        {
            return sql;
        }

        var identityColumn = GetIdentityColumn<TReadModel>();
        var identityProperty = GetIdentityProperty<TReadModel>();
        var versionColumn = GetVersionColumn<TReadModel>();

        var versionCheck = string.IsNullOrEmpty(versionColumn)
            ? string.Empty
            : $"AND {versionColumn} = @_PREVIOUS_VERSION";

        var properties = GetPropertyInfos(readModelType);
        var updateColumns = properties
            .Where(p => p.Name.ToSnakeCase() != identityColumn)
            .Select(p => $"{p.Name.ToSnakeCase()} = @{p.Name}");

        var tableName = GetTableName<TReadModel>();

        sql = $"UPDATE {tableName} SET {string.Join(", ", updateColumns)} WHERE {identityColumn} = @{identityProperty} {versionCheck}";

        _updateSqls[readModelType] = sql;

        return sql;
    }

    public string CreatePurgeSql<TReadModel>() where TReadModel : IReadModel
    {
        return _purgeSqls.GetOrCreate(typeof(TReadModel), t => $"DELETE FROM {GetTableName(t)}");
    }

    public string GetTableName<TReadModel>() where TReadModel : IReadModel
    {
        return GetTableName(typeof(TReadModel));
    }

    private static string GetTableName(Type readModelType)
    {
        return TableNames.GetOrAdd(
            readModelType,
            t =>
            {
                var tableAttribute = t.GetTypeInfo().GetCustomAttribute<TableAttribute>(false);
                var table = string.IsNullOrEmpty(tableAttribute?.Name)
                    ? $"ReadModel-{t.Name.Replace("ReadModel", string.Empty)}"
                    : tableAttribute.Name;
                return string.IsNullOrEmpty(tableAttribute?.Schema)
                    ? $"\"{table}\""
                    : $"\"{tableAttribute?.Schema}\".\"{table}\"";
            });
    }

    private static string GetIdentityColumn<TReadModel>()
    {
        return IdentityColumns.GetOrAdd(
            typeof(TReadModel),
            t =>
            {
                var propertyInfo = GetPropertyInfos(t)
                    .SingleOrDefault(
                        pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelIdentityColumnAttribute));
                return (propertyInfo?.Name ?? "AggregateId").ToSnakeCase();
            });
    }

    private static string GetIdentityProperty<TReadModel>()
    {
        return IdentityProperties.GetOrAdd(
            typeof(TReadModel),
            t =>
            {
                var propertyInfo = GetPropertyInfos(t)
                    .SingleOrDefault(
                        pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelIdentityColumnAttribute));
                return propertyInfo?.Name ?? "AggregateId";
            });
    }

    private static string GetVersionColumn<TReadModel>()
    {
        return VersionColumns.GetOrAdd(
            typeof(TReadModel),
            t =>
            {
                var propertyInfo = GetPropertyInfos(t)
                    .SingleOrDefault(
                        pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelVersionColumnAttribute));
                if (propertyInfo != null)
                {
                    return propertyInfo.Name.ToSnakeCase();
                }

                return GetPropertyInfos(t).Any(n => n.Name == "LastAggregateSequenceNumber")
                    ? "last_aggregate_sequence_number"
                    : string.Empty;
            });
    }

    private static IReadOnlyCollection<PropertyInfo> GetPropertyInfos(Type readModelType)
    {
        return PropertyInfos.GetOrAdd(
            readModelType,
            t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(p => !p.GetCustomAttributes().Any(a => a is SqlReadModelIgnoreColumnAttribute))
                .OrderBy(p => p.Name)
                .ToList());
    }
}
