/**
 * PostgreSQL AddressRepository. All operations are scoped by user_id, so a
 * caller can never read or mutate another user's address even with a valid id.
 */
import type pg from 'pg';
import type { AddressRepository } from '../domain/ports.js';
import type { Address, AddressPatch, NewAddress } from '../domain/address.js';

interface AddressRow {
  id: string;
  user_id: string;
  label: string | null;
  recipient_name: string;
  street: string;
  city: string;
  postal_code: string;
  country: string;
  is_default: boolean;
  created_at: Date;
  updated_at: Date;
}

/** Whitelist of updatable columns -> input key. Prevents SQL building from input. */
const UPDATABLE: ReadonlyArray<[keyof AddressPatch, string]> = [
  ['label', 'label'],
  ['recipientName', 'recipient_name'],
  ['street', 'street'],
  ['city', 'city'],
  ['postalCode', 'postal_code'],
  ['country', 'country'],
  ['isDefault', 'is_default'],
];

export class PgAddressRepository implements AddressRepository {
  constructor(private readonly pool: pg.Pool) {}

  async listByUserId(userId: string): Promise<Address[]> {
    const result = await this.pool.query<AddressRow>(
      `SELECT * FROM users_addresses WHERE user_id = $1
       ORDER BY is_default DESC, created_at`,
      [userId],
    );
    return result.rows.map(toAddress);
  }

  async create(userId: string, input: NewAddress): Promise<Address> {
    const result = await this.pool.query<AddressRow>(
      `INSERT INTO users_addresses
         (user_id, label, recipient_name, street, city, postal_code, country, is_default)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
       RETURNING *`,
      [
        userId,
        input.label ?? null,
        input.recipientName,
        input.street,
        input.city,
        input.postalCode,
        input.country,
        input.isDefault ?? false,
      ],
    );
    const row = result.rows[0];
    if (!row) {
      throw new Error('users_addresses INSERT returned no row');
    }
    return toAddress(row);
  }

  async update(
    userId: string,
    addressId: string,
    patch: AddressPatch,
  ): Promise<Address | undefined> {
    const setClauses: string[] = [];
    const values: unknown[] = [];
    for (const [key, column] of UPDATABLE) {
      const value = patch[key];
      if (value !== undefined) {
        values.push(value);
        setClauses.push(`${column} = $${values.length}`);
      }
    }
    if (setClauses.length === 0) {
      return this.findByIdScoped(userId, addressId);
    }

    values.push(userId, addressId);
    const result = await this.pool.query<AddressRow>(
      `UPDATE users_addresses SET ${setClauses.join(', ')}, updated_at = now()
       WHERE user_id = $${values.length - 1} AND id = $${values.length}
       RETURNING *`,
      values,
    );
    const row = result.rows[0];
    return row ? toAddress(row) : undefined;
  }

  async delete(userId: string, addressId: string): Promise<boolean> {
    const result = await this.pool.query(
      `DELETE FROM users_addresses WHERE user_id = $1 AND id = $2`,
      [userId, addressId],
    );
    return (result.rowCount ?? 0) > 0;
  }

  private async findByIdScoped(userId: string, addressId: string): Promise<Address | undefined> {
    const result = await this.pool.query<AddressRow>(
      `SELECT * FROM users_addresses WHERE user_id = $1 AND id = $2`,
      [userId, addressId],
    );
    const row = result.rows[0];
    return row ? toAddress(row) : undefined;
  }
}

function toAddress(row: AddressRow): Address {
  return {
    id: row.id,
    userId: row.user_id,
    label: row.label,
    recipientName: row.recipient_name,
    street: row.street,
    city: row.city,
    postalCode: row.postal_code,
    country: row.country,
    isDefault: row.is_default,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}
