import { Pool, PoolClient } from "pg";
import { EventEmitter } from "events";
import {
  IDatabaseAdapter,
  IUrl,
  PostgresConfig,
} from "../interfaces/database.interface";

export class PostgresAdapter extends EventEmitter implements IDatabaseAdapter {
  private pool: Pool | null = null;
  private config: PostgresConfig;
  private tableName: string;
  private isConnectedFlag: boolean = false;

  constructor(config: PostgresConfig) {
    super();
    this.config = {
      maxRetries: 5,
      retryDelay: 5000,
      ...config,
    };
    this.tableName = config.tableName || "urls";
  }

  async connect(): Promise<boolean> {
    try {
      if (this.isConnectedFlag && this.pool) {
        return true;
      }

      this.pool = new Pool({
        host: this.config.host,
        port: this.config.port,
        database: this.config.database,
        user: this.config.username,
        password: this.config.password,
        max: 20, // Maximum number of clients in the pool
        idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
        connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
      });

      // Test the connection
      const client = await this.pool.connect();
      client.release();

      await this.createTables();
      this.isConnectedFlag = true;
      this.emit("connected");
      return true;
    } catch (error) {
      this.emit("error", error);
      return false;
    }
  }

  async createTables(): Promise<void> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const createTableSQL = `
      CREATE TABLE IF NOT EXISTS ${quotedTableName} (
        id SERIAL PRIMARY KEY,
        urlId VARCHAR(255) UNIQUE NOT NULL,
        originalUrl TEXT NOT NULL,
        shortUrl TEXT NOT NULL,
        clicks INTEGER DEFAULT 0,
        createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        expiresAt TIMESTAMP NULL
      );
    `;

    const createIndexesSQL = `
      CREATE INDEX IF NOT EXISTS idx_${this.tableName}_urlId ON ${quotedTableName}(urlId);
      CREATE INDEX IF NOT EXISTS idx_${this.tableName}_originalUrl ON ${quotedTableName}(originalUrl);
      CREATE INDEX IF NOT EXISTS idx_${this.tableName}_createdAt ON ${quotedTableName}(createdAt);
    `;

    const client = await this.pool.connect();
    try {
      await client.query(createTableSQL);
      await client.query(createIndexesSQL);
    } finally {
      client.release();
    }
  }

  async disconnect(): Promise<void> {
    if (this.pool) {
      await this.pool.end();
      this.pool = null;
      this.isConnectedFlag = false;
      this.emit("disconnected");
    }
  }

  isConnected(): boolean {
    return this.isConnectedFlag && this.pool !== null;
  }

  async createUrl(url: Omit<IUrl, "createdAt">): Promise<IUrl> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const sql = `
      INSERT INTO ${quotedTableName} (urlId, originalUrl, shortUrl, clicks, expiresAt)
      VALUES ($1, $2, $3, $4, $5)
      RETURNING *
    `;

    const client = await this.pool.connect();
    try {
      const result = await client.query(sql, [
        url.urlId,
        url.originalUrl,
        url.shortUrl,
        url.clicks,
        url.expiresAt ? url.expiresAt.toISOString() : null,
      ]);

      const row = result.rows[0];
      return {
        urlId: row.urlid,
        originalUrl: row.originalurl,
        shortUrl: row.shorturl,
        clicks: row.clicks,
        createdAt: row.createdat,
        expiresAt: row.expiresat ? new Date(row.expiresat) : undefined,
      };
    } finally {
      client.release();
    }
  }

  async findUrlByUrlId(urlId: string): Promise<IUrl | null> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const sql = `SELECT * FROM ${quotedTableName} WHERE urlId = $1 LIMIT 1`;
    const client = await this.pool.connect();

    try {
      const result = await client.query(sql, [urlId]);
      const row = result.rows[0];

      if (!row) return null;

      return {
        urlId: row.urlid,
        originalUrl: row.originalurl,
        shortUrl: row.shorturl,
        clicks: row.clicks,
        createdAt: row.createdat,
        expiresAt: row.expiresat ? new Date(row.expiresat) : undefined,
      };
    } finally {
      client.release();
    }
  }

  async findUrlByOriginalUrl(originalUrl: string): Promise<IUrl | null> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const sql = `SELECT * FROM ${quotedTableName} WHERE originalUrl = $1 LIMIT 1`;
    const client = await this.pool.connect();

    try {
      const result = await client.query(sql, [originalUrl]);
      const row = result.rows[0];

      if (!row) return null;

      return {
        urlId: row.urlid,
        originalUrl: row.originalurl,
        shortUrl: row.shorturl,
        clicks: row.clicks,
        createdAt: row.createdat,
        expiresAt: row.expiresat ? new Date(row.expiresat) : undefined,
      };
    } finally {
      client.release();
    }
  }

  async updateUrlClicks(urlId: string, clicks: number): Promise<void> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const sql = `UPDATE ${quotedTableName} SET clicks = $1 WHERE urlId = $2`;
    const client = await this.pool.connect();

    try {
      await client.query(sql, [clicks, urlId]);
    } finally {
      client.release();
    }
  }

  async deleteUrl(urlId: string): Promise<boolean> {
    if (!this.pool) throw new Error("Database not connected");

    // Quote table name to preserve case
    const quotedTableName = `"${this.tableName}"`;

    const sql = `DELETE FROM ${quotedTableName} WHERE urlId = $1`;
    const client = await this.pool.connect();

    try {
      const result = await client.query(sql, [urlId]);
      return (result.rowCount || 0) > 0;
    } finally {
      client.release();
    }
  }
}
