import type pg from 'pg';
import { ProductBrandNotFoundError, ProductSlugAlreadyExistsError } from '../domain/errors.js';
import type {
  NewProduct,
  Product,
  ProductAttribute,
  ProductPatch,
  ProductState,
} from '../domain/product.js';
import type { ProductRepository } from '../domain/ports.js';

export interface ProductRow {
  id: string;
  name: string;
  slug: string;
  description: string | null;
  state: ProductState;
  channels: 'online' | 'offline' | 'all';
  featured: boolean;
  // JSONB from PostgreSQL; parsed as string[] by pg driver
  attributes: unknown;
  seo_title: string | null;
  seo_description: string | null;
  brand_id: string | null;
  category_ids: string[] | null;
  brand_name: string | null;
  brand_slug: string | null;
  expiration_date: string | null;
  unit_weight_kg: string | number;
  min_purchase_qty: number;
  ai_assisted: boolean;
  created_at: Date;
  updated_at: Date;
}

const UNIQUE_VIOLATION = '23505';
const FOREIGN_KEY_VIOLATION = '23503';

export const PRODUCT_COLUMNS = `
  p.*,
  COALESCE(
    array_agg(pc.category_id ORDER BY pc.category_id) FILTER (WHERE pc.category_id IS NOT NULL),
    ARRAY[]::uuid[]
  ) AS category_ids,
  b.name AS brand_name,
  b.slug AS brand_slug
`;

const UPDATABLE: ReadonlyArray<[keyof ProductPatch, string]> = [
  ['name', 'name'],
  ['slug', 'slug'],
  ['description', 'description'],
  ['state', 'state'],
  ['channels', 'channels'],
  ['featured', 'featured'],
  ['attributes', 'attributes'],
  ['seoTitle', 'seo_title'],
  ['seoDescription', 'seo_description'],
  ['brandId', 'brand_id'],
  ['expirationDate', 'expiration_date'],
  ['unitWeightKg', 'unit_weight_kg'],
  ['aiAssisted', 'ai_assisted'],
  ['minPurchaseQty', 'min_purchase_qty'],
];

export class PgProductRepository implements ProductRepository {
  constructor(private readonly pool: pg.Pool) {}

  async findById(id: string): Promise<Product | undefined> {
    const result = await this.pool.query<ProductRow>(
      `SELECT ${PRODUCT_COLUMNS}
       FROM catalog_products p
       LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
       LEFT JOIN brands_brands b ON b.id = p.brand_id
       WHERE p.id = $1
       GROUP BY p.id, b.name, b.slug`,
      [id],
    );
    const row = result.rows[0];
    return row ? toProduct(row) : undefined;
  }

  async findActiveBySlug(slug: string): Promise<Product | undefined> {
    const result = await this.pool.query<ProductRow>(
      `SELECT ${PRODUCT_COLUMNS}
       FROM catalog_products p
       LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
       LEFT JOIN brands_brands b ON b.id = p.brand_id
       WHERE p.slug = $1 AND p.state = 'active'
       GROUP BY p.id, b.name, b.slug`,
      [slug],
    );
    const row = result.rows[0];
    return row ? toProduct(row) : undefined;
  }

  async create(input: NewProduct): Promise<Product> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');
      const result = await client.query<ProductRow>(
        `INSERT INTO catalog_products (name, slug, description, state, seo_title, seo_description, brand_id, expiration_date, unit_weight_kg, min_purchase_qty, ai_assisted)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
         RETURNING *, ARRAY[]::uuid[] AS category_ids`,
        [
          input.name,
          input.slug,
          input.description ?? null,
          input.state ?? 'draft',
          input.seoTitle ?? null,
          input.seoDescription ?? null,
          input.brandId ?? null,
          input.expirationDate ?? null,
          input.unitWeightKg ?? 1,
          input.minPurchaseQty ?? 1,
          input.aiAssisted ?? false,
        ],
      );
      const row = result.rows[0];
      if (!row) {
        throw new Error('catalog_products INSERT returned no row');
      }
      await this.replaceCategories(client, row.id, input.categoryIds ?? []);
      await client.query('COMMIT');
      return (await this.findById(row.id)) ?? toProduct(row);
    } catch (error) {
      await client.query('ROLLBACK');
      throw mapPgError(error);
    } finally {
      client.release();
    }
  }

  async update(id: string, patch: ProductPatch): Promise<Product | undefined> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');
      const setClauses: string[] = [];
      const values: unknown[] = [];
      for (const [key, column] of UPDATABLE) {
        if (key in patch) {
          // The `attributes` column is JSONB; pg-node would otherwise serialise
          // JS arrays as PG array literals (`{bio,keto}`) which the JSONB parser
          // rejects. Stringify explicitly and cast to JSONB.
          if (key === 'attributes' && Array.isArray(patch[key])) {
            values.push(JSON.stringify(patch[key]));
            setClauses.push(`${column} = $${values.length}::jsonb`);
          } else {
            values.push(patch[key]);
            setClauses.push(`${column} = $${values.length}`);
          }
        }
      }

      let exists = true;
      if (setClauses.length > 0) {
        values.push(id);
        const result = await client.query(
          `UPDATE catalog_products SET ${setClauses.join(', ')}, updated_at = now()
           WHERE id = $${values.length}`,
          values,
        );
        exists = (result.rowCount ?? 0) > 0;
      } else {
        const found = await client.query('SELECT 1 FROM catalog_products WHERE id = $1', [id]);
        exists = (found.rowCount ?? 0) > 0;
      }

      if (!exists) {
        await client.query('COMMIT');
        return undefined;
      }
      if (patch.categoryIds !== undefined) {
        await this.replaceCategories(client, id, patch.categoryIds);
      }
      await client.query('COMMIT');
      return this.findById(id);
    } catch (error) {
      await client.query('ROLLBACK');
      throw mapPgError(error);
    } finally {
      client.release();
    }
  }

  async delete(id: string): Promise<void> {
    await this.pool.query('DELETE FROM catalog_products WHERE id = $1', [id]);
  }

  async listAll(options?: {
    limit?: number;
    offset?: number;
    q?: string;
  }): Promise<{ items: Product[]; total: number }> {
    const limit = options?.limit ?? 20;
    const offset = options?.offset ?? 0;
    const q = options?.q?.trim();

    const countResult = await this.pool.query<{ count: string }>(
      q
        ? `SELECT COUNT(*)
           FROM catalog_products p
           WHERE p.name ILIKE $1
              OR EXISTS (
                SELECT 1 FROM catalog_product_variants v
                WHERE v.product_id = p.id AND v.ean ILIKE $1
              )`
        : 'SELECT COUNT(*) FROM catalog_products',
      q ? [`%${q}%`] : [],
    );
    const total = parseInt(countResult.rows[0]?.count ?? '0', 10);

    const rows = await this.pool.query<ProductRow>(
      q
        ? `SELECT ${PRODUCT_COLUMNS}
           FROM catalog_products p
           LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
           LEFT JOIN brands_brands b ON b.id = p.brand_id
           WHERE p.name ILIKE $1
              OR EXISTS (
                SELECT 1 FROM catalog_product_variants v
                WHERE v.product_id = p.id AND v.ean ILIKE $1
              )
           GROUP BY p.id, b.name, b.slug
           ORDER BY p.created_at DESC
           LIMIT $2 OFFSET $3`
        : `SELECT ${PRODUCT_COLUMNS}
           FROM catalog_products p
           LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
           LEFT JOIN brands_brands b ON b.id = p.brand_id
           GROUP BY p.id, b.name, b.slug
           ORDER BY p.created_at DESC
           LIMIT $1 OFFSET $2`,
      q ? [`%${q}%`, limit, offset] : [limit, offset],
    );

    return { items: rows.rows.map(toProduct), total };
  }

  async brandExists(brandId: string): Promise<boolean> {
    const result = await this.pool.query<{ exists: boolean }>(
      'SELECT EXISTS (SELECT 1 FROM brands_brands WHERE id = $1)',
      [brandId],
    );
    return result.rows[0]?.exists ?? false;
  }

  async categoriesExist(categoryIds: readonly string[]): Promise<boolean> {
    const uniqueIds = [...new Set(categoryIds)];
    if (uniqueIds.length === 0) {
      return true;
    }
    const result = await this.pool.query<{ count: string }>(
      'SELECT count(*)::int AS count FROM categories_categories WHERE id = ANY($1::uuid[])',
      [uniqueIds],
    );
    return Number(result.rows[0]?.count ?? 0) === uniqueIds.length;
  }

  private async replaceCategories(
    client: pg.PoolClient,
    productId: string,
    categoryIds: readonly string[],
  ): Promise<void> {
    await client.query('DELETE FROM catalog_product_categories WHERE product_id = $1', [productId]);
    const uniqueIds = [...new Set(categoryIds)];
    for (const categoryId of uniqueIds) {
      await client.query(
        'INSERT INTO catalog_product_categories (product_id, category_id) VALUES ($1, $2)',
        [productId, categoryId],
      );
    }
  }
}

export function toProduct(row: ProductRow): Product {
  return {
    id: row.id,
    name: row.name,
    slug: row.slug,
    description: row.description,
    state: row.state,
    channels: row.channels,
    featured: row.featured,
    attributes: Array.isArray(row.attributes)
      ? (row.attributes as unknown[]).filter(
          (a): a is ProductAttribute =>
            typeof a === 'string' &&
            [
              'bio',
              'comercio-justo',
              'congelado',
              'cruelty-free',
              'de-temporada',
              'demeter',
              'fruta-verdura',
              'keto',
              'kosher',
              'low-carb',
              'raw-food',
              'sin-azucar',
              'sin-gluten',
              'sin-lactosa',
              'vegano',
              'zero-waste',
            ].includes(a),
        )
      : [],
    seoTitle: row.seo_title,
    seoDescription: row.seo_description,
    brandId: row.brand_id,
    brand:
      row.brand_id && row.brand_name && row.brand_slug
        ? { id: row.brand_id, name: row.brand_name, slug: row.brand_slug }
        : undefined,
    categoryIds: row.category_ids ?? [],
    expirationDate: row.expiration_date ?? null,
    unitWeightKg: Number(row.unit_weight_kg ?? 1),
    aiAssisted: row.ai_assisted ?? false,
    minPurchaseQty: row.min_purchase_qty ?? 1,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

function mapPgError(error: unknown): Error {
  if (isPgError(error, UNIQUE_VIOLATION)) {
    return new ProductSlugAlreadyExistsError();
  }
  if (isPgError(error, FOREIGN_KEY_VIOLATION)) {
    return new ProductBrandNotFoundError();
  }
  return error instanceof Error ? error : new Error('Unknown product repository error');
}

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