import type pg from 'pg';
import type {
  ClubMember,
  ClubResolveInput,
  ClubSettings,
  ClubTransaction,
  RecordClubTransactionCommand,
  RecordClubTransactionResult,
  UpdateClubSettingsCommand,
} from '../domain/club.js';
import { DEFAULT_CLUB_SETTINGS } from '../domain/club.js';
import { ClubInsufficientBalanceError } from '../domain/errors.js';
import type { ClubRepository, CreateClubMemberCommand } from '../domain/ports.js';

interface SettingRow {
  key: string;
  value: string;
}

interface MemberRow {
  id: string;
  user_id: string | null;
  member_code: string;
  status: 'active' | 'blocked' | 'merged';
  tier_code: string;
  current_balance_cents: number;
  created_at: Date;
  updated_at: Date;
}

interface TransactionRow {
  id: string;
  member_id: string;
  sale_id: string | null;
  store_id: string | null;
  type: 'earn' | 'redeem' | 'refund' | 'bonus' | 'adjustment';
  amount_cents: number;
  balance_delta_cents: number;
  idempotency_key: string | null;
  metadata: Record<string, unknown> | null;
  created_at: Date;
}

type Queryable = Pick<pg.Pool, 'query'> | Pick<pg.PoolClient, 'query'>;

const CLUB_SETTING_KEYS = {
  enabled: 'club_enabled',
  cashbackBps: 'club_cashback_bps',
  allowAnonymousMembers: 'club_allow_anonymous_members',
  allowRecoveryCodes: 'club_allow_recovery_codes',
  minimumRedeemAmountCents: 'club_minimum_redeem_cents',
} as const;

export class PgClubRepository implements ClubRepository {
  constructor(private readonly pool: pg.Pool) {}

  async getSettings(): Promise<ClubSettings> {
    const result = await this.pool.query<SettingRow>(
      `SELECT key, value FROM store_settings WHERE key = ANY($1::text[])`,
      [Object.values(CLUB_SETTING_KEYS)],
    );
    return parseSettings(result.rows);
  }

  async updateSettings(
    input: UpdateClubSettingsCommand,
    updatedBy: string | null,
  ): Promise<ClubSettings> {
    const updates: Array<[string, string]> = [];
    if (input.enabled !== undefined) {
      updates.push([CLUB_SETTING_KEYS.enabled, String(input.enabled)]);
    }
    if (input.cashbackBps !== undefined) {
      updates.push([CLUB_SETTING_KEYS.cashbackBps, String(input.cashbackBps)]);
    }
    if (input.allowAnonymousMembers !== undefined) {
      updates.push([
        CLUB_SETTING_KEYS.allowAnonymousMembers,
        String(input.allowAnonymousMembers),
      ]);
    }
    if (input.allowRecoveryCodes !== undefined) {
      updates.push([CLUB_SETTING_KEYS.allowRecoveryCodes, String(input.allowRecoveryCodes)]);
    }
    if (input.minimumRedeemAmountCents !== undefined) {
      updates.push([
        CLUB_SETTING_KEYS.minimumRedeemAmountCents,
        String(input.minimumRedeemAmountCents),
      ]);
    }

    for (const [key, value] of updates) {
      await this.pool.query(
        `INSERT INTO store_settings (key, value, updated_by) VALUES ($1, $2, $3)
         ON CONFLICT (key) DO UPDATE
           SET value = EXCLUDED.value,
               updated_at = NOW(),
               updated_by = EXCLUDED.updated_by`,
        [key, value, updatedBy],
      );
    }
    return this.getSettings();
  }

  async createMemberWithDevice(input: CreateClubMemberCommand): Promise<ClubMember | null> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');
      const memberResult = await client.query<MemberRow>(
        `INSERT INTO club_members (user_id, member_code, status, tier_code, current_balance_cents)
         VALUES ($1, $2, 'active', 'base', 0)
         ON CONFLICT (member_code) DO NOTHING
         RETURNING *`,
        [input.userId ?? null, input.memberCode],
      );
      const row = memberResult.rows[0];
      if (!row) {
        await client.query('ROLLBACK');
        return null;
      }
      await client.query(
        `INSERT INTO club_devices (member_id, device_token_hash)
         VALUES ($1, $2)`,
        [row.id, input.deviceTokenHash],
      );
      await client.query('COMMIT');
      return toMember(row);
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async findMemberByUserId(userId: string): Promise<ClubMember | null> {
    const result = await this.pool.query<MemberRow>(
      `SELECT * FROM club_members
        WHERE user_id = $1 AND status <> 'merged'
        LIMIT 1`,
      [userId],
    );
    return result.rows[0] ? toMember(result.rows[0]) : null;
  }

  async findMemberByCode(code: string): Promise<ClubMember | null> {
    const result = await this.pool.query<MemberRow>(
      `SELECT * FROM club_members
        WHERE member_code = $1 AND status = 'active'
        LIMIT 1`,
      [code],
    );
    return result.rows[0] ? toMember(result.rows[0]) : null;
  }

  async findMemberByDeviceTokenHash(hash: string, touch = false): Promise<ClubMember | null> {
    const result = await this.pool.query<MemberRow>(
      `SELECT member.*
         FROM club_devices device
         JOIN club_members member ON member.id = device.member_id
        WHERE device.device_token_hash = $1
          AND device.revoked_at IS NULL
          AND member.status <> 'merged'
        LIMIT 1`,
      [hash],
    );
    const row = result.rows[0];
    if (!row) return null;
    if (touch) {
      await this.pool.query(
        `UPDATE club_devices SET last_used_at = now()
          WHERE device_token_hash = $1 AND revoked_at IS NULL`,
        [hash],
      );
    }
    return toMember(row);
  }

  async resolveMember(
    input: ClubResolveInput & { deviceTokenHash?: string | null },
  ): Promise<ClubMember | null> {
    if (input.userId) {
      const byUser = await this.findMemberByUserId(input.userId);
      if (byUser) return byUser;
    }
    if (input.deviceTokenHash) {
      return this.findMemberByDeviceTokenHash(input.deviceTokenHash, true);
    }
    return null;
  }

  async listTransactions(memberId: string, limit: number): Promise<ClubTransaction[]> {
    const result = await this.pool.query<TransactionRow>(
      `SELECT * FROM club_transactions
        WHERE member_id = $1
        ORDER BY created_at DESC, id DESC
        LIMIT $2`,
      [memberId, limit],
    );
    return result.rows.map(toTransaction);
  }

  async recordTransaction(
    input: RecordClubTransactionCommand,
  ): Promise<RecordClubTransactionResult> {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');

      if (input.idempotencyKey) {
        const existing = await client.query<TransactionRow>(
          `SELECT * FROM club_transactions WHERE idempotency_key = $1`,
          [input.idempotencyKey],
        );
        const existingRow = existing.rows[0];
        if (existingRow) {
          const member = await client.query<MemberRow>(
            `SELECT * FROM club_members WHERE id = $1`,
            [existingRow.member_id],
          );
          const memberRow = member.rows[0];
          if (!memberRow) {
            throw new Error(`club_members row not found for transaction ${existingRow.id}`);
          }
          await client.query('COMMIT');
          return {
            member: toMember(memberRow),
            transaction: toTransaction(existingRow),
            created: false,
          };
        }
      }

      const memberResult = await client.query<MemberRow>(
        `SELECT * FROM club_members WHERE id = $1 FOR UPDATE`,
        [input.memberId],
      );
      const member = memberResult.rows[0];
      if (!member) {
        throw new Error(`club_members row not found: ${input.memberId}`);
      }

      const nextBalance = Number(member.current_balance_cents) + input.balanceDeltaCents;
      if (nextBalance < 0) {
        throw new ClubInsufficientBalanceError();
      }

      const transactionResult = await client.query<TransactionRow>(
        `INSERT INTO club_transactions (
           member_id, sale_id, store_id, type, amount_cents,
           balance_delta_cents, idempotency_key, metadata
         ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
         RETURNING *`,
        [
          input.memberId,
          input.saleId ?? null,
          input.storeId ?? null,
          input.type,
          input.amountCents,
          input.balanceDeltaCents,
          input.idempotencyKey ?? null,
          JSON.stringify(input.metadata ?? {}),
        ],
      );
      const updatedMember = await client.query<MemberRow>(
        `UPDATE club_members
            SET current_balance_cents = $2,
                updated_at = now()
          WHERE id = $1
          RETURNING *`,
        [input.memberId, nextBalance],
      );
      const updatedMemberRow = updatedMember.rows[0];
      const transactionRow = transactionResult.rows[0];
      if (!updatedMemberRow || !transactionRow) {
        throw new Error('Club transaction persistence returned no row');
      }

      await client.query('COMMIT');
      return {
        member: toMember(updatedMemberRow),
        transaction: toTransaction(transactionRow),
        created: true,
      };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
}

function parseSettings(rows: SettingRow[]): ClubSettings {
  const map = new Map(rows.map((row) => [row.key, row.value]));
  return {
    enabled: parseBoolean(map.get(CLUB_SETTING_KEYS.enabled), DEFAULT_CLUB_SETTINGS.enabled),
    cashbackBps: parseInteger(map.get(CLUB_SETTING_KEYS.cashbackBps), DEFAULT_CLUB_SETTINGS.cashbackBps),
    allowAnonymousMembers: parseBoolean(
      map.get(CLUB_SETTING_KEYS.allowAnonymousMembers),
      DEFAULT_CLUB_SETTINGS.allowAnonymousMembers,
    ),
    allowRecoveryCodes: parseBoolean(
      map.get(CLUB_SETTING_KEYS.allowRecoveryCodes),
      DEFAULT_CLUB_SETTINGS.allowRecoveryCodes,
    ),
    minimumRedeemAmountCents: parseInteger(
      map.get(CLUB_SETTING_KEYS.minimumRedeemAmountCents),
      DEFAULT_CLUB_SETTINGS.minimumRedeemAmountCents,
    ),
  };
}

function parseBoolean(raw: string | undefined, fallback: boolean): boolean {
  if (raw === undefined) return fallback;
  return raw === 'true';
}

function parseInteger(raw: string | undefined, fallback: number): number {
  if (raw === undefined) return fallback;
  const parsed = Number(raw);
  return Number.isInteger(parsed) ? parsed : fallback;
}

function toMember(row: MemberRow): ClubMember {
  return {
    id: row.id,
    userId: row.user_id,
    memberCode: row.member_code,
    status: row.status,
    tierCode: row.tier_code,
    currentBalanceCents: Number(row.current_balance_cents),
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

function toTransaction(row: TransactionRow): ClubTransaction {
  return {
    id: row.id,
    memberId: row.member_id,
    saleId: row.sale_id,
    storeId: row.store_id,
    type: row.type,
    amountCents: Number(row.amount_cents),
    balanceDeltaCents: Number(row.balance_delta_cents),
    idempotencyKey: row.idempotency_key,
    metadata: row.metadata ?? {},
    createdAt: row.created_at,
  };
}
