import type pg from 'pg';
import { ProductImageMainAlreadyExistsError } from '../domain/errors.js';
import type {
  NewProductImage,
  ProductImage,
  ProductImageOrderItem,
  ProductImageStorage,
} from '../domain/image.js';
import type { ProductImageRepository } from '../domain/ports.js';

interface ImageRow {
  id: string;
  product_id: string;
  variant_id: string | null;
  url: string;
  alt_text: string;
  position: number;
  role: 'main' | 'gallery';
  created_at: Date;
  updated_at: Date;
}

const UNIQUE_VIOLATION = '23505';

export class PgProductImageRepository implements ProductImageRepository {
  constructor(
    private readonly pool: pg.Pool,
    private readonly storage: ProductImageStorage,
  ) {}

  async listByProductId(
    productId: string,
    variantId: string | null = null,
  ): Promise<ProductImage[]> {
    const result = await this.pool.query<ImageRow>(
      `SELECT * FROM catalog_product_images
       WHERE product_id = $1 AND variant_id IS NOT DISTINCT FROM $2
       ORDER BY position ASC, created_at ASC, id ASC`,
      [productId, variantId],
    );
    return result.rows.map(toImage);
  }

  async listByProductIds(productIds: readonly string[]): Promise<ProductImage[]> {
    if (productIds.length === 0) return [];
    const result = await this.pool.query<ImageRow>(
      `SELECT * FROM catalog_product_images
       WHERE product_id = ANY($1::uuid[])
       ORDER BY product_id ASC, position ASC, created_at ASC, id ASC`,
      [productIds as string[]],
    );
    return result.rows.map(toImage);
  }

  async attach(productId: string, input: NewProductImage): Promise<ProductImage> {
    try {
      const result = await this.pool.query<ImageRow>(
        `INSERT INTO catalog_product_images (product_id, variant_id, url, alt_text, position, role)
         VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING *`,
        [
          productId,
          input.variantId ?? null,
          this.storage.normalizeUrl(input.url),
          input.altText,
          input.position ?? 0,
          input.role,
        ],
      );
      const row = result.rows[0];
      if (!row) {
        throw new Error('catalog_product_images INSERT returned no row');
      }
      return toImage(row);
    } catch (error) {
      if (isPgError(error, UNIQUE_VIOLATION)) {
        throw new ProductImageMainAlreadyExistsError();
      }
      throw error;
    }
  }

  async detach(productId: string, imageId: string): Promise<boolean> {
    const result = await this.pool.query(
      'DELETE FROM catalog_product_images WHERE product_id = $1 AND id = $2',
      [productId, imageId],
    );
    return (result.rowCount ?? 0) > 0;
  }

  async reorder(
    productId: string,
    items: readonly ProductImageOrderItem[],
  ): Promise<ProductImage[]> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');
      const scope = await this.assertSingleScope(client, productId, items);
      for (const item of items) {
        await client.query(
          `UPDATE catalog_product_images SET position = $1, updated_at = now()
           WHERE product_id = $2 AND id = $3`,
          [item.position, productId, item.imageId],
        );
      }
      await client.query('COMMIT');
      return this.listByProductId(productId, scope);
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async variantBelongsToProduct(productId: string, variantId: string): Promise<boolean> {
    const result = await this.pool.query<{ exists: boolean }>(
      'SELECT EXISTS (SELECT 1 FROM catalog_product_variants WHERE id = $1 AND product_id = $2)',
      [variantId, productId],
    );
    return result.rows[0]?.exists ?? false;
  }

  private async assertSingleScope(
    client: pg.PoolClient,
    productId: string,
    items: readonly ProductImageOrderItem[],
  ): Promise<string | null> {
    const imageIds = items.map((item) => item.imageId);
    const result = await client.query<{ variant_id: string | null }>(
      'SELECT variant_id FROM catalog_product_images WHERE product_id = $1 AND id = ANY($2::uuid[])',
      [productId, imageIds],
    );
    if (result.rows.length !== imageIds.length) {
      throw new Error('Product image reorder item not found');
    }
    const [first] = result.rows;
    const scope = first?.variant_id ?? null;
    if (!result.rows.every((row) => row.variant_id === scope)) {
      throw new Error('Product image reorder items must belong to the same scope');
    }
    return scope;
  }
}

function toImage(row: ImageRow): ProductImage {
  return {
    id: row.id,
    productId: row.product_id,
    variantId: row.variant_id,
    url: row.url,
    altText: row.alt_text,
    position: row.position,
    role: row.role,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

function isPgError(error: unknown, code: string): boolean {
  return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
}
