import pg from "pg";
import { createId } from "./security.js";

const { Pool } = pg;

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL ?? "postgres://cents:cents-dev-only@127.0.0.1:55432/cents",
  max: 10,
});

const schema = `
CREATE TABLE IF NOT EXISTS users (
  id text PRIMARY KEY,
  name varchar(40) NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 40),
  name_key varchar(40),
  password_hash text,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE users ADD COLUMN IF NOT EXISTS name_key varchar(40);
ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash text;
UPDATE users SET name_key = lower(trim(name)) WHERE name_key IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS users_name_key_unique ON users (name_key);

CREATE TABLE IF NOT EXISTS ledgers (
  id text PRIMARY KEY,
  name varchar(40) NOT NULL,
  color varchar(16) NOT NULL DEFAULT '#087f72',
  theme varchar(24) NOT NULL DEFAULT 'jade',
  icon varchar(32) NOT NULL DEFAULT 'wallet',
  default_currency varchar(3) NOT NULL DEFAULT 'CNY',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  archived_at timestamptz
);

ALTER TABLE ledgers ADD COLUMN IF NOT EXISTS theme varchar(24) NOT NULL DEFAULT 'jade';
ALTER TABLE ledgers ADD COLUMN IF NOT EXISTS icon varchar(32) NOT NULL DEFAULT 'wallet';
UPDATE ledgers SET theme = CASE lower(color)
  WHEN '#3478e5' THEN 'ocean'
  WHEN '#7559d9' THEN 'berry'
  WHEN '#f05d3f' THEN 'coral'
  WHEN '#d84486' THEN 'berry'
  WHEN '#15956d' THEN 'meadow'
  ELSE 'jade'
END WHERE theme = 'jade' AND lower(color) <> '#087f72';
ALTER TABLE ledgers ADD COLUMN IF NOT EXISTS personal_owner_id text REFERENCES users(id) ON DELETE CASCADE;
CREATE UNIQUE INDEX IF NOT EXISTS ledgers_personal_owner_unique
  ON ledgers (personal_owner_id) WHERE personal_owner_id IS NOT NULL;

CREATE TABLE IF NOT EXISTS ledger_members (
  ledger_id text NOT NULL REFERENCES ledgers(id) ON DELETE CASCADE,
  user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  role varchar(8) NOT NULL CHECK (role IN ('owner', 'member')),
  joined_at timestamptz NOT NULL DEFAULT now(),
  removed_at timestamptz,
  PRIMARY KEY (ledger_id, user_id)
);

CREATE TABLE IF NOT EXISTS ledger_invitations (
  id text PRIMARY KEY,
  ledger_id text NOT NULL REFERENCES ledgers(id) ON DELETE CASCADE,
  key_hash char(64) NOT NULL UNIQUE,
  role varchar(8) NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'member')),
  created_by text REFERENCES users(id) ON DELETE SET NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz NOT NULL,
  accepted_by text REFERENCES users(id) ON DELETE SET NULL,
  accepted_at timestamptz,
  revoked_at timestamptz
);

CREATE TABLE IF NOT EXISTS app_invitations (
  id text PRIMARY KEY,
  key_hash char(64) NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz NOT NULL,
  accepted_by text REFERENCES users(id) ON DELETE SET NULL,
  accepted_at timestamptz,
  revoked_at timestamptz
);

CREATE INDEX IF NOT EXISTS app_invitations_lookup
  ON app_invitations (key_hash, expires_at);

CREATE INDEX IF NOT EXISTS ledger_invitations_lookup
  ON ledger_invitations (key_hash, expires_at);

CREATE TABLE IF NOT EXISTS sessions (
  id text PRIMARY KEY,
  user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token_hash char(64) NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz NOT NULL,
  last_seen_at timestamptz NOT NULL DEFAULT now(),
  revoked_at timestamptz
);

CREATE INDEX IF NOT EXISTS sessions_lookup ON sessions (token_hash, expires_at);

CREATE TABLE IF NOT EXISTS entries (
  id text PRIMARY KEY,
  owner_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  type varchar(8) NOT NULL CHECK (type IN ('expense', 'income')),
  amount integer NOT NULL CHECK (amount > 0),
  currency varchar(3) NOT NULL,
  base_currency varchar(3) NOT NULL DEFAULT 'CNY',
  base_amount integer CHECK (base_amount > 0),
  exchange_rate varchar(32),
  exchange_rate_date date,
  exchange_rate_effective_date date,
  exchange_rate_source varchar(8) NOT NULL CHECK (exchange_rate_source IN ('manual', 'system')),
  conversion_status varchar(8) NOT NULL DEFAULT 'pending',
  reimbursement_of_entry_id text REFERENCES entries(id) ON DELETE RESTRICT,
  category_id varchar(100) NOT NULL,
  note varchar(500) NOT NULL DEFAULT '',
  occurred_at timestamptz NOT NULL,
  created_by text REFERENCES users(id) ON DELETE SET NULL,
  updated_by text REFERENCES users(id) ON DELETE SET NULL,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL,
  deleted_at timestamptz,
  version integer NOT NULL CHECK (version > 0),
  server_updated_at timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE entries ADD COLUMN IF NOT EXISTS owner_id text REFERENCES users(id) ON DELETE CASCADE;
UPDATE entries SET owner_id = COALESCE(created_by, updated_by) WHERE owner_id IS NULL;
ALTER TABLE entries ALTER COLUMN owner_id SET NOT NULL;
ALTER TABLE entries ALTER COLUMN base_amount DROP NOT NULL;
ALTER TABLE entries ALTER COLUMN exchange_rate DROP NOT NULL;
ALTER TABLE entries ADD COLUMN IF NOT EXISTS exchange_rate_date date;
ALTER TABLE entries ADD COLUMN IF NOT EXISTS exchange_rate_effective_date date;
ALTER TABLE entries ADD COLUMN IF NOT EXISTS conversion_status varchar(8) NOT NULL DEFAULT 'pending';
ALTER TABLE entries ADD COLUMN IF NOT EXISTS reimbursement_of_entry_id text REFERENCES entries(id) ON DELETE RESTRICT;
UPDATE entries
   SET base_currency = 'CNY',
       base_amount = amount,
       exchange_rate = '1',
       exchange_rate_date = COALESCE(exchange_rate_date, (occurred_at AT TIME ZONE 'Asia/Shanghai')::date),
       exchange_rate_effective_date = COALESCE(exchange_rate_effective_date, (occurred_at AT TIME ZONE 'Asia/Shanghai')::date),
       exchange_rate_source = 'system',
       conversion_status = 'exact'
 WHERE currency = 'CNY';
UPDATE entries
   SET base_currency = 'CNY',
       base_amount = NULL,
       exchange_rate = NULL,
       exchange_rate_date = COALESCE(exchange_rate_date, (occurred_at AT TIME ZONE 'Asia/Shanghai')::date),
       exchange_rate_effective_date = NULL,
       exchange_rate_source = 'system',
       conversion_status = 'pending'
 WHERE currency <> 'CNY'
   AND exchange_rate = '1'
   AND exchange_rate_source = 'manual';
UPDATE entries
   SET base_currency = 'CNY',
       exchange_rate_effective_date = COALESCE(
         exchange_rate_effective_date,
         exchange_rate_date,
         (occurred_at AT TIME ZONE 'Asia/Shanghai')::date
       ),
       conversion_status = 'fallback'
 WHERE currency <> 'CNY'
   AND base_amount IS NOT NULL
   AND exchange_rate IS NOT NULL
   AND conversion_status = 'pending';
UPDATE entries
   SET exchange_rate_date = (occurred_at AT TIME ZONE 'Asia/Shanghai')::date
 WHERE exchange_rate_date IS NULL;
ALTER TABLE entries ALTER COLUMN exchange_rate_date SET NOT NULL;
ALTER TABLE entries DROP CONSTRAINT IF EXISTS entries_conversion_status_check;
ALTER TABLE entries ADD CONSTRAINT entries_conversion_status_check
  CHECK (conversion_status IN ('exact', 'fallback', 'pending'));
ALTER TABLE entries DROP CONSTRAINT IF EXISTS entries_conversion_values_check;
ALTER TABLE entries ADD CONSTRAINT entries_conversion_values_check
  CHECK (
    (conversion_status = 'pending' AND base_amount IS NULL AND exchange_rate IS NULL)
    OR
    (conversion_status IN ('exact', 'fallback') AND base_amount > 0 AND exchange_rate IS NOT NULL)
  );
ALTER TABLE entries DROP CONSTRAINT IF EXISTS entries_reimbursement_shape_check;
ALTER TABLE entries ADD CONSTRAINT entries_reimbursement_shape_check
  CHECK (
    reimbursement_of_entry_id IS NULL
    OR (type = 'income' AND reimbursement_of_entry_id <> id)
  );

CREATE TABLE IF NOT EXISTS entry_ledgers (
  entry_id text NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
  ledger_id text NOT NULL REFERENCES ledgers(id) ON DELETE CASCADE,
  linked_at timestamptz NOT NULL DEFAULT now(),
  unlinked_at timestamptz,
  updated_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (entry_id, ledger_id)
);

ALTER TABLE entry_ledgers ADD COLUMN IF NOT EXISTS unlinked_at timestamptz;
ALTER TABLE entry_ledgers ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();

DO $$
BEGIN
  IF EXISTS (
    SELECT 1 FROM information_schema.columns
     WHERE table_schema = 'public' AND table_name = 'entries' AND column_name = 'ledger_id'
  ) THEN
    EXECUTE 'INSERT INTO entry_ledgers (entry_id, ledger_id)
             SELECT id, ledger_id FROM entries
             ON CONFLICT (entry_id, ledger_id) DO NOTHING';
  END IF;
END $$;

DROP INDEX IF EXISTS entries_ledger_occurred;
ALTER TABLE entries DROP COLUMN IF EXISTS ledger_id;

CREATE INDEX IF NOT EXISTS entries_owner_occurred
  ON entries (owner_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS entries_reimbursement_of
  ON entries (reimbursement_of_entry_id)
  WHERE reimbursement_of_entry_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS entry_ledgers_ledger
  ON entry_ledgers (ledger_id, entry_id);

CREATE TABLE IF NOT EXISTS entry_sync_operations (
  id text PRIMARY KEY,
  user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  processed_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS exchange_rates (
  currency varchar(3) NOT NULL,
  rate_date date NOT NULL,
  cny_per_unit numeric(24, 12) NOT NULL CHECK (cny_per_unit > 0),
  provider varchar(64) NOT NULL,
  provider_rate_date date NOT NULL,
  fetched_at timestamptz NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (currency, rate_date)
);

CREATE INDEX IF NOT EXISTS exchange_rates_currency_date
  ON exchange_rates (currency, rate_date DESC);

CREATE TABLE IF NOT EXISTS entry_conversion_jobs (
  entry_id text PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE,
  target_version integer NOT NULL,
  attempts integer NOT NULL DEFAULT 0,
  available_at timestamptz NOT NULL DEFAULT now(),
  locked_at timestamptz,
  lock_token text,
  last_error text,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS entry_conversion_jobs_available
  ON entry_conversion_jobs (available_at, created_at);
`;

export async function initializeDatabase() {
  await pool.query(schema);
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    const users = await client.query<{ id: string }>(
      `SELECT u.id FROM users u
        WHERE NOT EXISTS (SELECT 1 FROM ledgers l WHERE l.personal_owner_id = u.id)
        FOR UPDATE`,
    );
    for (const user of users.rows) {
      const ledgerId = createId();
      await client.query(
        `INSERT INTO ledgers (id, name, color, default_currency, personal_owner_id)
         VALUES ($1, '个人账本', '#087f72', 'CNY', $2)`,
        [ledgerId, user.id],
      );
      await client.query(
        "INSERT INTO ledger_members (ledger_id, user_id, role) VALUES ($1, $2, 'owner')",
        [ledgerId, user.id],
      );
    }
    await client.query(
      `INSERT INTO entry_ledgers (entry_id, ledger_id)
       SELECT e.id, l.id FROM entries e JOIN ledgers l ON l.personal_owner_id = e.owner_id
       ON CONFLICT (entry_id, ledger_id) DO UPDATE
         SET unlinked_at = NULL, updated_at = now()
       WHERE entry_ledgers.unlinked_at IS NOT NULL`,
    );
    await client.query(
      `INSERT INTO entry_conversion_jobs (entry_id, target_version)
       SELECT id, version FROM entries
        WHERE conversion_status IN ('pending', 'fallback') AND deleted_at IS NULL
       ON CONFLICT (entry_id) DO NOTHING`,
    );
    await client.query("COMMIT");
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
}
