import type pg from 'pg';
import type { Category, CategoryPatch, NewCategory } from '../domain/category.js';
import { CategorySlugAlreadyExistsError } from '../domain/errors.js';
import type { CategoryRepository } from '../domain/ports.js';

interface CategoryRow {
  id: string;
  parent_id: string | null;
  name: string;
  slug: string;
  seo_title: string | null;
  seo_description: string | null;
  description: string | null;
  is_parent: boolean;
  emoji: string | null;
  color: string | null;
  bg_color: string | null;
  text_color: string | null;
  created_at: Date;
  updated_at: Date;
}

const UNIQUE_VIOLATION = '23505';

const UPDATABLE: ReadonlyArray<[keyof CategoryPatch, string]> = [
  ['parentId', 'parent_id'],
  ['name', 'name'],
  ['slug', 'slug'],
  ['seoTitle', 'seo_title'],
  ['seoDescription', 'seo_description'],
  ['description', 'description'],
  ['isParent', 'is_parent'],
  ['emoji', 'emoji'],
  ['color', 'color'],
  ['bgColor', 'bg_color'],
  ['textColor', 'text_color'],
];

export class PgCategoryRepository implements CategoryRepository {
  constructor(private readonly pool: pg.Pool) {}

  async list(): Promise<Category[]> {
    const result = await this.pool.query<CategoryRow>(
      `SELECT * FROM categories_categories ORDER BY parent_id NULLS FIRST, name, created_at`,
    );
    return result.rows.map(toCategory);
  }

  async findById(id: string): Promise<Category | undefined> {
    const result = await this.pool.query<CategoryRow>(
      'SELECT * FROM categories_categories WHERE id = $1',
      [id],
    );
    const row = result.rows[0];
    return row ? toCategory(row) : undefined;
  }

  async findBySlug(slug: string): Promise<Category | undefined> {
    const result = await this.pool.query<CategoryRow>(
      'SELECT * FROM categories_categories WHERE slug = $1',
      [slug],
    );
    const row = result.rows[0];
    return row ? toCategory(row) : undefined;
  }

  async create(input: NewCategory): Promise<Category> {
    try {
      const result = await this.pool.query<CategoryRow>(
        `INSERT INTO categories_categories (parent_id, name, slug, seo_title, seo_description, description, is_parent, emoji, color, bg_color, text_color)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
         RETURNING *`,
        [
          input.parentId ?? null,
          input.name,
          input.slug,
          input.seoTitle ?? null,
          input.seoDescription ?? null,
          input.description ?? null,
          input.isParent ?? false,
          input.emoji ?? null,
          input.color ?? null,
          input.bgColor ?? null,
          input.textColor ?? null,
        ],
      );
      const row = result.rows[0];
      if (!row) {
        throw new Error('categories_categories INSERT returned no row');
      }
      return toCategory(row);
    } catch (error) {
      if (isPgError(error, UNIQUE_VIOLATION)) {
        throw new CategorySlugAlreadyExistsError();
      }
      throw error;
    }
  }

  async update(id: string, patch: CategoryPatch): Promise<Category | undefined> {
    const setClauses: string[] = [];
    const values: unknown[] = [];
    for (const [key, column] of UPDATABLE) {
      if (key in patch) {
        values.push(patch[key]);
        setClauses.push(`${column} = $${values.length}`);
      }
    }
    if (setClauses.length === 0) {
      return this.findById(id);
    }

    values.push(id);
    try {
      const result = await this.pool.query<CategoryRow>(
        `UPDATE categories_categories SET ${setClauses.join(', ')}, updated_at = now()
         WHERE id = $${values.length}
         RETURNING *`,
        values,
      );
      const row = result.rows[0];
      return row ? toCategory(row) : undefined;
    } catch (error) {
      if (isPgError(error, UNIQUE_VIOLATION)) {
        throw new CategorySlugAlreadyExistsError();
      }
      throw error;
    }
  }

  async delete(id: string): Promise<boolean> {
    const result = await this.pool.query('DELETE FROM categories_categories WHERE id = $1', [id]);
    return (result.rowCount ?? 0) > 0;
  }

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

  async isDescendant(candidateAncestorId: string, candidateDescendantId: string): Promise<boolean> {
    const result = await this.pool.query<{ exists: boolean }>(
      `WITH RECURSIVE descendants AS (
         SELECT id FROM categories_categories WHERE parent_id = $1
         UNION ALL
         SELECT c.id FROM categories_categories c
         INNER JOIN descendants d ON c.parent_id = d.id
       )
       SELECT EXISTS (SELECT 1 FROM descendants WHERE id = $2)`,
      [candidateAncestorId, candidateDescendantId],
    );
    return result.rows[0]?.exists ?? false;
  }
}

function toCategory(row: CategoryRow): Category {
  return {
    id: row.id,
    parentId: row.parent_id,
    name: row.name,
    slug: row.slug,
    seoTitle: row.seo_title,
    seoDescription: row.seo_description,
    description: row.description,
    isParent: row.is_parent,
    emoji: row.emoji,
    color: row.color,
    bgColor: row.bg_color,
    textColor: row.text_color,
    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;
}
