import 'dart:io';

import 'package:postgres/postgres.dart';

final class PostgresConnectionConfig {
  const PostgresConnectionConfig({
    required this.host,
    required this.port,
    required this.database,
    required this.username,
    required this.password,
    this.sslMode = SslMode.disable,
  });

  factory PostgresConnectionConfig.fromEnvironment([
    Map<String, String>? environment,
  ]) {
    final env = environment ?? Platform.environment;
    return PostgresConnectionConfig(
      host: _requiredEnv(env, 'DATABASE_HOST'),
      port: int.tryParse(env['DATABASE_PORT'] ?? '') ?? 5432,
      database: _requiredEnv(env, 'DATABASE_NAME'),
      username: _requiredEnv(env, 'DATABASE_USER'),
      password: _requiredEnv(env, 'DATABASE_PASSWORD'),
    );
  }

  final String host;
  final int port;
  final String database;
  final String username;
  final String password;
  final SslMode sslMode;
}

final class PostgresConnectionFactory {
  const PostgresConnectionFactory(this.config) : databaseUrl = null;

  const PostgresConnectionFactory.fromDatabaseUrl(this.databaseUrl)
    : config = null;

  final PostgresConnectionConfig? config;
  final String? databaseUrl;

  Future<Connection> open() {
    final databaseUrl = this.databaseUrl;
    if (databaseUrl != null) {
      return Connection.openFromUrl(databaseUrl);
    }
    final config = this.config!;
    return Connection.open(
      Endpoint(
        host: config.host,
        port: config.port,
        database: config.database,
        username: config.username,
        password: config.password,
      ),
      settings: ConnectionSettings(sslMode: config.sslMode),
    );
  }
}

final class PostgresMigrator {
  const PostgresMigrator(this.connection);

  final Connection connection;

  Future<void> applyMigrations({String migrationsPath = 'migrations'}) async {
    final directory = Directory(migrationsPath);
    if (!await directory.exists()) {
      throw StateError('Migrations directory not found: $migrationsPath');
    }

    final files =
        await directory
              .list()
              .where((entity) => entity is File && entity.path.endsWith('.sql'))
              .cast<File>()
              .toList()
          ..sort((left, right) => left.path.compareTo(right.path));

    for (final file in files) {
      final sql = await file.readAsString();
      for (final statement in _splitSqlStatements(sql)) {
        await connection.execute(statement);
      }
    }
  }
}

String _requiredEnv(Map<String, String> environment, String key) {
  final value = environment[key];
  if (value == null || value.trim().isEmpty) {
    throw StateError('Missing required environment variable: $key');
  }
  return value;
}

List<String> _splitSqlStatements(String sql) {
  final statements = <String>[];
  final buffer = StringBuffer();
  var inSingleQuote = false;
  var inDoubleQuote = false;
  String? dollarQuoteTag;

  for (var index = 0; index < sql.length; index++) {
    final char = sql[index];
    final next = index + 1 < sql.length ? sql[index + 1] : null;

    if (dollarQuoteTag != null) {
      if (sql.startsWith(dollarQuoteTag, index)) {
        buffer.write(dollarQuoteTag);
        index += dollarQuoteTag.length - 1;
        dollarQuoteTag = null;
      } else {
        buffer.write(char);
      }
      continue;
    }

    if (!inSingleQuote && !inDoubleQuote && char == r'$') {
      final tag = _readDollarQuoteTag(sql, index);
      if (tag != null) {
        dollarQuoteTag = tag;
        buffer.write(tag);
        index += tag.length - 1;
        continue;
      }
    }

    if (!inDoubleQuote && char == "'") {
      buffer.write(char);
      if (inSingleQuote && next == "'") {
        index++;
        buffer.write(next);
      } else {
        inSingleQuote = !inSingleQuote;
      }
      continue;
    }

    if (!inSingleQuote && char == '"') {
      inDoubleQuote = !inDoubleQuote;
      buffer.write(char);
      continue;
    }

    if (!inSingleQuote && !inDoubleQuote && char == ';') {
      final statement = buffer.toString().trim();
      if (statement.isNotEmpty) {
        statements.add(statement);
      }
      buffer.clear();
      continue;
    }

    buffer.write(char);
  }

  final statement = buffer.toString().trim();
  if (statement.isNotEmpty) {
    statements.add(statement);
  }
  return statements;
}

String? _readDollarQuoteTag(String sql, int start) {
  final end = sql.indexOf(r'$', start + 1);
  if (end == -1) {
    return null;
  }
  final tagName = sql.substring(start + 1, end);
  final validTag = RegExp(r'^[A-Za-z_][A-Za-z0-9_]*$|^$').hasMatch(tagName);
  return validTag ? sql.substring(start, end + 1) : null;
}
