BEGIN;

CREATE TABLE IF NOT EXISTS companies (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  nif VARCHAR(32) NOT NULL,
  rc VARCHAR(64) NOT NULL,
  nis VARCHAR(64),
  ai VARCHAR(64),
  industry_sector VARCHAR(255),
  registration_date DATE,
  address TEXT,
  city VARCHAR(120),
  phone VARCHAR(50),
  email VARCHAR(255),
  logo_url TEXT,
  currency VARCHAR(10) NOT NULL DEFAULT 'DZD',
  active_modules TEXT[] NOT NULL DEFAULT ARRAY['SCF']::TEXT[],
  fiscal_year_start DATE NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

ALTER TABLE companies
  ADD COLUMN IF NOT EXISTS nis VARCHAR(64);

ALTER TABLE companies
  ADD COLUMN IF NOT EXISTS ai VARCHAR(64);

ALTER TABLE companies
  ADD COLUMN IF NOT EXISTS industry_sector VARCHAR(255);

ALTER TABLE companies
  ADD COLUMN IF NOT EXISTS registration_date DATE;

ALTER TABLE companies
  ADD COLUMN IF NOT EXISTS active_modules TEXT[] NOT NULL DEFAULT ARRAY['SCF']::TEXT[];

DO $$
BEGIN
  ALTER TABLE companies
    DROP CONSTRAINT IF EXISTS chk_companies_active_modules;

  ALTER TABLE companies
    ADD CONSTRAINT chk_companies_active_modules CHECK (
      active_modules <@ ARRAY['SCF', 'MGX', 'TRACKING', 'ALL']::TEXT[]
      AND array_length(active_modules, 1) >= 1
    );
END $$;

CREATE UNIQUE INDEX IF NOT EXISTS idx_companies_nif_unique ON companies (nif);
CREATE UNIQUE INDEX IF NOT EXISTS idx_companies_rc_unique ON companies (rc);

CREATE TABLE IF NOT EXISTS roles (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE,
  permissions JSONB NOT NULL DEFAULT '[]'::jsonb
);

CREATE TABLE IF NOT EXISTS users (
  id BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  full_name VARCHAR(255) NOT NULL,
  role VARCHAR(50) NOT NULL REFERENCES roles (name) ON UPDATE CASCADE,
  company_id BIGINT REFERENCES companies (id) ON DELETE SET NULL,
  allowed_modules TEXT[] NOT NULL DEFAULT ARRAY['SCF']::TEXT[],
  default_module VARCHAR(10) NOT NULL DEFAULT 'SCF',
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  last_login TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS failed_login_attempts INTEGER NOT NULL DEFAULT 0;

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ;

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS allowed_modules TEXT[] NOT NULL DEFAULT ARRAY['SCF']::TEXT[];

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS default_module VARCHAR(10) NOT NULL DEFAULT 'SCF';

ALTER TABLE users
  DROP CONSTRAINT IF EXISTS chk_users_allowed_modules;

ALTER TABLE users
  ADD CONSTRAINT chk_users_allowed_modules CHECK (
    allowed_modules <@ ARRAY['SCF', 'MGX', 'TRACKING', 'ALL']::TEXT[]
    AND array_length(allowed_modules, 1) >= 1
  );

ALTER TABLE users
  DROP CONSTRAINT IF EXISTS chk_users_default_module;

ALTER TABLE users
  ADD CONSTRAINT chk_users_default_module CHECK (
    default_module IN ('SCF', 'MGX', 'TRACKING', 'BOTH')
  );

CREATE INDEX IF NOT EXISTS idx_users_company_id ON users (company_id);
CREATE INDEX IF NOT EXISTS idx_users_role ON users (role);
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users (is_active);
CREATE INDEX IF NOT EXISTS idx_users_locked_at ON users (locked_at);

CREATE TABLE IF NOT EXISTS user_companies (
  user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (user_id, company_id)
);

CREATE INDEX IF NOT EXISTS idx_user_companies_company_id ON user_companies (company_id);

INSERT INTO user_companies (user_id, company_id)
SELECT id, company_id
FROM users
WHERE company_id IS NOT NULL
ON CONFLICT (user_id, company_id) DO NOTHING;

CREATE TABLE IF NOT EXISTS fiscal_years (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  label VARCHAR(50) NOT NULL,
  start_date DATE NOT NULL,
  end_date DATE NOT NULL,
  is_closed BOOLEAN NOT NULL DEFAULT FALSE,
  is_current BOOLEAN NOT NULL DEFAULT FALSE,
  CONSTRAINT chk_fiscal_year_dates CHECK (end_date >= start_date),
  CONSTRAINT uq_fiscal_years_company_label UNIQUE (company_id, label)
);

CREATE INDEX IF NOT EXISTS idx_fiscal_years_company_id ON fiscal_years (company_id);
CREATE INDEX IF NOT EXISTS idx_fiscal_years_is_current ON fiscal_years (company_id, is_current);

CREATE TABLE IF NOT EXISTS account_classes (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  code VARCHAR(10) NOT NULL,
  label VARCHAR(255) NOT NULL,
  CONSTRAINT uq_account_classes_company_code UNIQUE (company_id, code)
);

CREATE INDEX IF NOT EXISTS idx_account_classes_company_id ON account_classes (company_id);

CREATE TABLE IF NOT EXISTS accounts (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  code VARCHAR(20) NOT NULL,
  label VARCHAR(255) NOT NULL,
  account_type VARCHAR(50) NOT NULL,
  parent_id BIGINT REFERENCES accounts (id) ON DELETE SET NULL,
  is_movable BOOLEAN NOT NULL DEFAULT TRUE,
  is_auxiliary BOOLEAN NOT NULL DEFAULT FALSE,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  balance_normal_side VARCHAR(10) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_accounts_company_code UNIQUE (company_id, code),
  CONSTRAINT chk_accounts_balance_side CHECK (balance_normal_side IN ('debit', 'credit'))
);

CREATE INDEX IF NOT EXISTS idx_accounts_company_id ON accounts (company_id);
CREATE INDEX IF NOT EXISTS idx_accounts_parent_id ON accounts (parent_id);
CREATE INDEX IF NOT EXISTS idx_accounts_type ON accounts (company_id, account_type);
CREATE INDEX IF NOT EXISTS idx_accounts_active ON accounts (company_id, is_active);

CREATE TABLE IF NOT EXISTS auxiliary_accounts (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  account_id BIGINT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE,
  third_party_type VARCHAR(20) NOT NULL,
  ref_code VARCHAR(50) NOT NULL,
  name VARCHAR(255) NOT NULL,
  address TEXT,
  nif VARCHAR(32),
  rc VARCHAR(64),
  nis VARCHAR(64),
  ai VARCHAR(64),
  industry_sector VARCHAR(255),
  registration_date DATE,
  city VARCHAR(120),
  wilaya VARCHAR(120),
  postal_code VARCHAR(20),
  phone VARCHAR(50),
  email VARCHAR(255),
  fax VARCHAR(50),
  principal_contact VARCHAR(255),
  credit_limit NUMERIC(18, 2) NOT NULL DEFAULT 0,
  payment_terms INTEGER NOT NULL DEFAULT 30,
  preferred_payment_method VARCHAR(20),
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_auxiliary_accounts_company_ref UNIQUE (company_id, ref_code),
  CONSTRAINT chk_auxiliary_accounts_type CHECK (third_party_type IN ('client', 'supplier'))
);

ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS rc VARCHAR(64);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS nis VARCHAR(64);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS ai VARCHAR(64);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS industry_sector VARCHAR(255);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS registration_date DATE;
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS city VARCHAR(120);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS wilaya VARCHAR(120);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS fax VARCHAR(50);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS principal_contact VARCHAR(255);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS preferred_payment_method VARCHAR(20);
ALTER TABLE auxiliary_accounts ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;

CREATE INDEX IF NOT EXISTS idx_auxiliary_accounts_company_id ON auxiliary_accounts (company_id);
CREATE INDEX IF NOT EXISTS idx_auxiliary_accounts_account_id ON auxiliary_accounts (account_id);
CREATE INDEX IF NOT EXISTS idx_auxiliary_accounts_type ON auxiliary_accounts (company_id, third_party_type);
CREATE INDEX IF NOT EXISTS idx_auxiliary_accounts_active ON auxiliary_accounts (company_id, third_party_type, is_active);

CREATE TABLE IF NOT EXISTS journals (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  code VARCHAR(20) NOT NULL,
  label VARCHAR(255) NOT NULL,
  journal_type VARCHAR(20) NOT NULL,
  default_counterpart_account BIGINT REFERENCES accounts (id) ON DELETE SET NULL,
  numbering_mode VARCHAR(20) NOT NULL DEFAULT 'continuous',
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  CONSTRAINT uq_journals_company_code UNIQUE (company_id, code),
  CONSTRAINT chk_journals_type CHECK (
    journal_type IN ('purchase', 'sale', 'cash', 'bank', 'misc', 'payroll', 'assets')
  ),
  CONSTRAINT chk_journals_numbering_mode CHECK (
    numbering_mode IN ('continuous', 'manual')
  )
);

CREATE INDEX IF NOT EXISTS idx_journals_company_id ON journals (company_id);
CREATE INDEX IF NOT EXISTS idx_journals_type ON journals (company_id, journal_type);

CREATE TABLE IF NOT EXISTS accounting_periods (
  id BIGSERIAL PRIMARY KEY,
  fiscal_year_id BIGINT NOT NULL REFERENCES fiscal_years (id) ON DELETE CASCADE,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  month SMALLINT NOT NULL,
  year INTEGER NOT NULL,
  is_closed BOOLEAN NOT NULL DEFAULT FALSE,
  closed_at TIMESTAMPTZ,
  closed_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  CONSTRAINT uq_accounting_periods_fiscal_month UNIQUE (fiscal_year_id, month, year),
  CONSTRAINT chk_accounting_periods_month CHECK (month BETWEEN 1 AND 12)
);

CREATE INDEX IF NOT EXISTS idx_accounting_periods_company_id ON accounting_periods (company_id);
CREATE INDEX IF NOT EXISTS idx_accounting_periods_fiscal_year_id ON accounting_periods (fiscal_year_id);
CREATE INDEX IF NOT EXISTS idx_accounting_periods_status ON accounting_periods (company_id, is_closed);

CREATE TABLE IF NOT EXISTS journal_entries (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  journal_id BIGINT NOT NULL REFERENCES journals (id) ON DELETE RESTRICT,
  fiscal_year_id BIGINT NOT NULL REFERENCES fiscal_years (id) ON DELETE RESTRICT,
  period_id BIGINT REFERENCES accounting_periods (id) ON DELETE SET NULL,
  entry_number VARCHAR(50) NOT NULL,
  entry_date DATE NOT NULL,
  label VARCHAR(255) NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'draft',
  piece_ref VARCHAR(100),
  attachment_url TEXT,
  created_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  validated_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_journal_entries_number UNIQUE (company_id, journal_id, fiscal_year_id, entry_number),
  CONSTRAINT chk_journal_entries_status CHECK (status IN ('draft', 'validated', 'cancelled'))
);

CREATE INDEX IF NOT EXISTS idx_journal_entries_company_id ON journal_entries (company_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_journal_id ON journal_entries (journal_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_fiscal_year_id ON journal_entries (fiscal_year_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_period_id ON journal_entries (period_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_entry_date ON journal_entries (company_id, entry_date DESC);
CREATE INDEX IF NOT EXISTS idx_journal_entries_status ON journal_entries (company_id, status);
CREATE INDEX IF NOT EXISTS idx_journal_entries_company_status_date
  ON journal_entries (company_id, status, entry_date DESC);

CREATE TABLE IF NOT EXISTS entry_lines (
  id BIGSERIAL PRIMARY KEY,
  entry_id BIGINT NOT NULL REFERENCES journal_entries (id) ON DELETE CASCADE,
  account_id BIGINT NOT NULL REFERENCES accounts (id) ON DELETE RESTRICT,
  auxiliary_account_id BIGINT REFERENCES auxiliary_accounts (id) ON DELETE SET NULL,
  label VARCHAR(255),
  debit NUMERIC(18, 2) NOT NULL DEFAULT 0,
  credit NUMERIC(18, 2) NOT NULL DEFAULT 0,
  due_date DATE,
  is_lettered BOOLEAN NOT NULL DEFAULT FALSE,
  letter_code VARCHAR(50),
  CONSTRAINT chk_entry_lines_positive CHECK (debit >= 0 AND credit >= 0),
  CONSTRAINT chk_entry_lines_balance CHECK (
    (debit > 0 AND credit = 0) OR (credit > 0 AND debit = 0)
  )
);

CREATE INDEX IF NOT EXISTS idx_entry_lines_entry_id ON entry_lines (entry_id);
CREATE INDEX IF NOT EXISTS idx_entry_lines_account_id ON entry_lines (account_id);
CREATE INDEX IF NOT EXISTS idx_entry_lines_auxiliary_account_id ON entry_lines (auxiliary_account_id);
CREATE INDEX IF NOT EXISTS idx_entry_lines_due_date ON entry_lines (due_date);
CREATE INDEX IF NOT EXISTS idx_entry_lines_letter_code ON entry_lines (letter_code);
CREATE INDEX IF NOT EXISTS idx_entry_lines_entry_account ON entry_lines (entry_id, account_id);

CREATE TABLE IF NOT EXISTS bank_accounts (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  account_id BIGINT REFERENCES accounts (id) ON DELETE SET NULL,
  bank_name VARCHAR(255) NOT NULL,
  account_number VARCHAR(100) NOT NULL,
  rib VARCHAR(100),
  currency VARCHAR(10) NOT NULL DEFAULT 'DZD',
  current_balance NUMERIC(18, 2) NOT NULL DEFAULT 0,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  CONSTRAINT uq_bank_accounts_company_account UNIQUE (company_id, account_number)
);

ALTER TABLE bank_accounts
  ADD COLUMN IF NOT EXISTS account_id BIGINT REFERENCES accounts (id) ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS idx_bank_accounts_company_id ON bank_accounts (company_id);
CREATE INDEX IF NOT EXISTS idx_bank_accounts_account_id ON bank_accounts (account_id);
CREATE INDEX IF NOT EXISTS idx_bank_accounts_active ON bank_accounts (company_id, is_active);

CREATE TABLE IF NOT EXISTS cash_registers (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  name VARCHAR(100) NOT NULL,
  currency VARCHAR(10) NOT NULL DEFAULT 'DZD',
  current_balance NUMERIC(18, 2) NOT NULL DEFAULT 0,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  CONSTRAINT uq_cash_registers_company_name UNIQUE (company_id, name)
);

CREATE INDEX IF NOT EXISTS idx_cash_registers_company_id ON cash_registers (company_id);
CREATE INDEX IF NOT EXISTS idx_cash_registers_active ON cash_registers (company_id, is_active);

CREATE TABLE IF NOT EXISTS reconciliations (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  bank_account_id BIGINT NOT NULL REFERENCES bank_accounts (id) ON DELETE CASCADE,
  reconciliation_date DATE NOT NULL,
  bank_balance NUMERIC(18, 2) NOT NULL DEFAULT 0,
  book_balance NUMERIC(18, 2) NOT NULL DEFAULT 0,
  difference NUMERIC(18, 2) NOT NULL DEFAULT 0,
  status VARCHAR(10) NOT NULL DEFAULT 'open',
  CONSTRAINT uq_reconciliations_bank_date UNIQUE (company_id, bank_account_id, reconciliation_date),
  CONSTRAINT chk_reconciliations_status CHECK (status IN ('open', 'closed'))
);

CREATE INDEX IF NOT EXISTS idx_reconciliations_company_id ON reconciliations (company_id);
CREATE INDEX IF NOT EXISTS idx_reconciliations_bank_account_id ON reconciliations (bank_account_id);
CREATE INDEX IF NOT EXISTS idx_reconciliations_status ON reconciliations (company_id, status);

CREATE TABLE IF NOT EXISTS bank_movements (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  bank_account_id BIGINT NOT NULL REFERENCES bank_accounts (id) ON DELETE CASCADE,
  movement_date DATE NOT NULL,
  operation_number VARCHAR(100),
  label VARCHAR(255) NOT NULL,
  amount NUMERIC(18, 2) NOT NULL,
  direction VARCHAR(10) NOT NULL,
  is_reconciled BOOLEAN NOT NULL DEFAULT FALSE,
  reconciliation_id BIGINT REFERENCES reconciliations (id) ON DELETE SET NULL,
  entry_id BIGINT REFERENCES journal_entries (id) ON DELETE SET NULL,
  CONSTRAINT chk_bank_movements_amount CHECK (amount > 0),
  CONSTRAINT chk_bank_movements_direction CHECK (direction IN ('debit', 'credit'))
);

ALTER TABLE bank_movements
  ADD COLUMN IF NOT EXISTS operation_number VARCHAR(100);

CREATE INDEX IF NOT EXISTS idx_bank_movements_company_id ON bank_movements (company_id);
CREATE INDEX IF NOT EXISTS idx_bank_movements_bank_account_id ON bank_movements (bank_account_id);
CREATE INDEX IF NOT EXISTS idx_bank_movements_entry_id ON bank_movements (entry_id);
CREATE INDEX IF NOT EXISTS idx_bank_movements_reconciliation_id ON bank_movements (reconciliation_id);
CREATE INDEX IF NOT EXISTS idx_bank_movements_date ON bank_movements (bank_account_id, movement_date DESC);
CREATE INDEX IF NOT EXISTS idx_bank_movements_reconciled ON bank_movements (bank_account_id, is_reconciled);
CREATE INDEX IF NOT EXISTS idx_bank_movements_operation_number ON bank_movements (bank_account_id, operation_number);

CREATE TABLE IF NOT EXISTS cash_movements (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  cash_register_id BIGINT NOT NULL REFERENCES cash_registers (id) ON DELETE CASCADE,
  movement_date DATE NOT NULL,
  label VARCHAR(255) NOT NULL,
  amount NUMERIC(18, 2) NOT NULL,
  direction VARCHAR(10) NOT NULL,
  piece_ref VARCHAR(100),
  entry_id BIGINT REFERENCES journal_entries (id) ON DELETE SET NULL,
  CONSTRAINT chk_cash_movements_amount CHECK (amount > 0),
  CONSTRAINT chk_cash_movements_direction CHECK (direction IN ('debit', 'credit'))
);

CREATE INDEX IF NOT EXISTS idx_cash_movements_company_id ON cash_movements (company_id);
CREATE INDEX IF NOT EXISTS idx_cash_movements_cash_register_id ON cash_movements (cash_register_id);
CREATE INDEX IF NOT EXISTS idx_cash_movements_entry_id ON cash_movements (entry_id);
CREATE INDEX IF NOT EXISTS idx_cash_movements_date ON cash_movements (cash_register_id, movement_date DESC);

CREATE TABLE IF NOT EXISTS matching (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  auxiliary_account_id BIGINT NOT NULL REFERENCES auxiliary_accounts (id) ON DELETE CASCADE,
  letter_code VARCHAR(50) NOT NULL,
  matched_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  matched_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  CONSTRAINT uq_matching_company_letter UNIQUE (company_id, auxiliary_account_id, letter_code)
);

CREATE INDEX IF NOT EXISTS idx_matching_company_id ON matching (company_id);
CREATE INDEX IF NOT EXISTS idx_matching_auxiliary_account_id ON matching (auxiliary_account_id);
CREATE INDEX IF NOT EXISTS idx_matching_matched_by ON matching (matched_by);

CREATE TABLE IF NOT EXISTS matching_lines (
  id BIGSERIAL PRIMARY KEY,
  matching_id BIGINT NOT NULL REFERENCES matching (id) ON DELETE CASCADE,
  entry_line_id BIGINT NOT NULL REFERENCES entry_lines (id) ON DELETE CASCADE,
  CONSTRAINT uq_matching_lines_entry_line UNIQUE (entry_line_id)
);

CREATE INDEX IF NOT EXISTS idx_matching_lines_matching_id ON matching_lines (matching_id);

CREATE TABLE IF NOT EXISTS reconciliation_entry_lines (
  id BIGSERIAL PRIMARY KEY,
  reconciliation_id BIGINT NOT NULL REFERENCES reconciliations (id) ON DELETE CASCADE,
  entry_line_id BIGINT NOT NULL REFERENCES entry_lines (id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_reconciliation_entry_line UNIQUE (entry_line_id)
);

CREATE INDEX IF NOT EXISTS idx_reconciliation_entry_lines_reconciliation_id
  ON reconciliation_entry_lines (reconciliation_id);

CREATE TABLE IF NOT EXISTS asset_categories (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  code VARCHAR(20) NOT NULL,
  label VARCHAR(255) NOT NULL,
  depreciation_method VARCHAR(20) NOT NULL DEFAULT 'linear',
  default_duration_years INTEGER NOT NULL,
  account_asset VARCHAR(20) NOT NULL,
  account_depreciation VARCHAR(20) NOT NULL,
  account_expense VARCHAR(20) NOT NULL,
  CONSTRAINT uq_asset_categories_company_code UNIQUE (company_id, code),
  CONSTRAINT chk_asset_categories_method CHECK (depreciation_method IN ('linear', 'degressive'))
);

CREATE INDEX IF NOT EXISTS idx_asset_categories_company_id ON asset_categories (company_id);

CREATE TABLE IF NOT EXISTS assets (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  category_id BIGINT NOT NULL REFERENCES asset_categories (id) ON DELETE RESTRICT,
  code VARCHAR(50) NOT NULL,
  label VARCHAR(255) NOT NULL,
  acquisition_date DATE NOT NULL,
  acquisition_value NUMERIC(18, 2) NOT NULL,
  residual_value NUMERIC(18, 2) NOT NULL DEFAULT 0,
  duration_years INTEGER NOT NULL,
  depreciation_method VARCHAR(20) NOT NULL,
  assignment_center VARCHAR(100),
  status VARCHAR(20) NOT NULL DEFAULT 'active',
  CONSTRAINT uq_assets_company_code UNIQUE (company_id, code),
  CONSTRAINT chk_assets_method CHECK (depreciation_method IN ('linear', 'degressive')),
  CONSTRAINT chk_assets_status CHECK (status IN ('active', 'sold', 'scrapped'))
);

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS description TEXT;

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS depreciation_start_date DATE;

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS disposal_date DATE;

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS disposal_type VARCHAR(20);

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS disposal_price NUMERIC(18, 2) NOT NULL DEFAULT 0;

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS disposal_gain_loss NUMERIC(18, 2) NOT NULL DEFAULT 0;

ALTER TABLE assets
  ADD COLUMN IF NOT EXISTS disposed_entry_id BIGINT REFERENCES journal_entries (id) ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS idx_assets_company_id ON assets (company_id);
CREATE INDEX IF NOT EXISTS idx_assets_category_id ON assets (category_id);
CREATE INDEX IF NOT EXISTS idx_assets_status ON assets (company_id, status);
CREATE INDEX IF NOT EXISTS idx_assets_depreciation_start_date ON assets (company_id, depreciation_start_date);
CREATE INDEX IF NOT EXISTS idx_assets_disposal_date ON assets (company_id, disposal_date);
CREATE INDEX IF NOT EXISTS idx_assets_disposed_entry_id ON assets (disposed_entry_id);

CREATE TABLE IF NOT EXISTS depreciation_lines (
  id BIGSERIAL PRIMARY KEY,
  asset_id BIGINT NOT NULL REFERENCES assets (id) ON DELETE CASCADE,
  fiscal_year_id BIGINT NOT NULL REFERENCES fiscal_years (id) ON DELETE RESTRICT,
  period_date DATE NOT NULL,
  amount_depreciation NUMERIC(18, 2) NOT NULL,
  cumulated_depreciation NUMERIC(18, 2) NOT NULL,
  net_book_value NUMERIC(18, 2) NOT NULL,
  entry_id BIGINT REFERENCES journal_entries (id) ON DELETE SET NULL,
  CONSTRAINT uq_depreciation_lines_asset_period UNIQUE (asset_id, fiscal_year_id, period_date)
);

CREATE INDEX IF NOT EXISTS idx_depreciation_lines_asset_id ON depreciation_lines (asset_id);
CREATE INDEX IF NOT EXISTS idx_depreciation_lines_fiscal_year_id ON depreciation_lines (fiscal_year_id);
CREATE INDEX IF NOT EXISTS idx_depreciation_lines_entry_id ON depreciation_lines (entry_id);

CREATE TABLE IF NOT EXISTS audit_log (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT REFERENCES companies (id) ON DELETE CASCADE,
  user_id BIGINT REFERENCES users (id) ON DELETE SET NULL,
  action VARCHAR(100) NOT NULL,
  entity_type VARCHAR(100) NOT NULL,
  entity_id BIGINT,
  old_data JSONB,
  new_data JSONB,
  ip_address INET,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_audit_log_company_id ON audit_log (company_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log (user_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log (entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log (created_at DESC);

CREATE TABLE IF NOT EXISTS login_history (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT REFERENCES companies (id) ON DELETE SET NULL,
  user_id BIGINT REFERENCES users (id) ON DELETE SET NULL,
  email VARCHAR(255),
  ip_address INET,
  user_agent TEXT,
  browser VARCHAR(255),
  result VARCHAR(20) NOT NULL,
  reason VARCHAR(255),
  attempted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT chk_login_history_result CHECK (result IN ('success', 'failure'))
);

CREATE INDEX IF NOT EXISTS idx_login_history_company_id ON login_history (company_id);
CREATE INDEX IF NOT EXISTS idx_login_history_user_id ON login_history (user_id);
CREATE INDEX IF NOT EXISTS idx_login_history_result ON login_history (result);
CREATE INDEX IF NOT EXISTS idx_login_history_attempted_at ON login_history (attempted_at DESC);

CREATE TABLE IF NOT EXISTS refresh_tokens (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  token_hash VARCHAR(128) NOT NULL UNIQUE,
  expires_at TIMESTAMPTZ NOT NULL,
  revoked_at TIMESTAMPTZ,
  replaced_by_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE SET NULL,
  ip_address INET,
  user_agent TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_revoked_at ON refresh_tokens (revoked_at);

CREATE TABLE IF NOT EXISTS notifications (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  type VARCHAR(60) NOT NULL,
  message TEXT NOT NULL,
  is_read BOOLEAN NOT NULL DEFAULT FALSE,
  entity_id BIGINT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications (user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications (user_id, is_read, created_at DESC);

CREATE TABLE IF NOT EXISTS user_push_tokens (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  push_token TEXT NOT NULL,
  platform VARCHAR(30),
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_user_push_tokens_token UNIQUE (push_token)
);

CREATE INDEX IF NOT EXISTS idx_user_push_tokens_user_id ON user_push_tokens (user_id, is_active);

CREATE TABLE IF NOT EXISTS system_settings (
  id SMALLINT PRIMARY KEY DEFAULT 1,
  application_name VARCHAR(255) NOT NULL DEFAULT 'Compta Control',
  default_language VARCHAR(10) NOT NULL DEFAULT 'fr',
  date_format VARCHAR(20) NOT NULL DEFAULT 'DD/MM/YYYY',
  decimal_separator VARCHAR(5) NOT NULL DEFAULT ',',
  thousands_separator VARCHAR(5) NOT NULL DEFAULT ' ',
  session_expiration_minutes INTEGER NOT NULL DEFAULT 480,
  max_failed_logins_before_lockout INTEGER NOT NULL DEFAULT 5,
  password_minimum_length INTEGER NOT NULL DEFAULT 8,
  last_backup_at TIMESTAMPTZ,
  last_backup_size_bytes BIGINT,
  updated_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT chk_system_settings_singleton CHECK (id = 1)
);

CREATE TABLE IF NOT EXISTS financial_statement_mapping (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  account_id BIGINT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE,
  statement_type VARCHAR(30) NOT NULL,
  line_code VARCHAR(50) NOT NULL,
  line_label VARCHAR(255) NOT NULL,
  sign SMALLINT NOT NULL DEFAULT 1,
  CONSTRAINT uq_fin_stmt_mapping UNIQUE (company_id, statement_type, line_code, account_id),
  CONSTRAINT chk_fin_stmt_mapping_type CHECK (
    statement_type IN ('balance_sheet', 'income_statement', 'cash_flow')
  ),
  CONSTRAINT chk_fin_stmt_mapping_sign CHECK (sign IN (-1, 1))
);

CREATE INDEX IF NOT EXISTS idx_fin_stmt_mapping_company_id ON financial_statement_mapping (company_id);
CREATE INDEX IF NOT EXISTS idx_fin_stmt_mapping_account_id ON financial_statement_mapping (account_id);
CREATE INDEX IF NOT EXISTS idx_fin_stmt_mapping_type ON financial_statement_mapping (company_id, statement_type);

CREATE TABLE IF NOT EXISTS financial_statement_notes (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  fiscal_year_id BIGINT NOT NULL REFERENCES fiscal_years (id) ON DELETE CASCADE,
  sections JSONB NOT NULL DEFAULT '{}'::jsonb,
  updated_by BIGINT REFERENCES users (id) ON DELETE SET NULL,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_financial_statement_notes UNIQUE (company_id, fiscal_year_id)
);

CREATE INDEX IF NOT EXISTS idx_fin_stmt_notes_company_id ON financial_statement_notes (company_id);
CREATE INDEX IF NOT EXISTS idx_fin_stmt_notes_fiscal_year_id ON financial_statement_notes (fiscal_year_id);

CREATE TABLE IF NOT EXISTS entry_templates (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,
  journal_id BIGINT NOT NULL REFERENCES journals (id) ON DELETE CASCADE,
  lines JSONB NOT NULL DEFAULT '[]'::jsonb,
  CONSTRAINT uq_entry_templates_company_name UNIQUE (company_id, name)
);

CREATE INDEX IF NOT EXISTS idx_entry_templates_company_id ON entry_templates (company_id);
CREATE INDEX IF NOT EXISTS idx_entry_templates_journal_id ON entry_templates (journal_id);

CREATE TABLE IF NOT EXISTS payment_methods (
  id BIGSERIAL PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE,
  code VARCHAR(20) NOT NULL,
  label VARCHAR(255) NOT NULL,
  type VARCHAR(20) NOT NULL,
  CONSTRAINT uq_payment_methods_company_code UNIQUE (company_id, code),
  CONSTRAINT chk_payment_methods_type CHECK (type IN ('cash', 'check', 'transfer', 'other'))
);

CREATE INDEX IF NOT EXISTS idx_payment_methods_company_id ON payment_methods (company_id);
CREATE INDEX IF NOT EXISTS idx_payment_methods_type ON payment_methods (company_id, type);

INSERT INTO roles (name, permissions)
VALUES
  ('super_admin', '["*"]'::jsonb),
  (
    'chief_accountant',
    '[
      "dashboard:read",
      "accounts:read",
      "accounts:create",
      "accounts:update",
      "accounts:delete",
      "journals:read",
      "journals:create",
      "journals:update",
      "journals:delete",
      "fiscal_years:read",
      "fiscal_years:create",
      "periods:read",
      "periods:close",
      "entries:read",
      "entries:create",
      "entries:update",
      "entries:delete",
      "entries:validate",
      "entries:export",
      "matching:read",
      "matching:create",
      "matching:update",
      "matching:export",
      "matching_admin",
      "reconciliation:read",
      "reconciliation:create",
      "reconciliation:update",
      "reconciliation:validate",
      "reconciliation:export",
      "closure:read",
      "closure:create",
      "closure:validate",
      "closure:reopen",
      "assets:read",
      "assets:create",
      "assets:update",
      "assets:delete",
      "assets:validate",
      "assets:export",
      "reports:read",
      "reports:export",
      "financial_statements:read",
      "financial_statements:export",
      "audit:read",
      "login_history:read"
    ]'::jsonb
  ),
  (
    'accountant',
    '[
      "dashboard:read",
      "accounts:read",
      "journals:read",
      "entries:read",
      "entries:create",
      "entries:update",
      "entries:export",
      "matching:read",
      "reports:read",
      "reports:export"
    ]'::jsonb
  ),
  (
    'management_controller',
    '[
      "dashboard:read",
      "reports:read",
      "reports:export",
      "financial_statements:read",
      "audit:read"
    ]'::jsonb
  ),
  (
    'cashier',
    '[
      "dashboard:read",
      "treasury:cash:read",
      "treasury:cash:create",
      "treasury:cash:update",
      "reports:read",
      "reports:export"
    ]'::jsonb
  ),
  (
    'treasurer',
    '[
      "dashboard:read",
      "treasury:cash:read",
      "treasury:cash:create",
      "treasury:cash:update",
      "treasury:bank:read",
      "treasury:bank:create",
      "treasury:bank:update",
      "reconciliation:read",
      "reconciliation:create",
      "reconciliation:update",
      "reconciliation:validate",
      "reconciliation:export",
      "reports:read",
      "reports:export"
    ]'::jsonb
  ),
  (
    'executive',
    '[
      "dashboard:read",
      "reports:read",
      "reports:export",
      "financial_statements:read",
      "financial_statements:export"
    ]'::jsonb
  ),
  (
    'manager',
    '[
      "dashboard:read",
      "reports:read",
      "entries:validate",
      "financial_statements:read",
      "matching_admin",
      "audit:read",
      "users:read",
      "users:create",
      "users:update",
      "users:delete",
      "roles:read",
      "roles:create",
      "roles:update",
      "system_settings:read",
      "system_settings:update",
      "backup:export",
      "closure:reopen"
    ]'::jsonb
  ),
  (
    'auditor',
    '[
      "audit:read",
      "login_history:read",
      "reports:read",
      "financial_statements:read"
    ]'::jsonb
  )
ON CONFLICT (name) DO UPDATE SET permissions = EXCLUDED.permissions;

INSERT INTO system_settings (
  id,
  application_name,
  default_language,
  date_format,
  decimal_separator,
  thousands_separator,
  session_expiration_minutes,
  max_failed_logins_before_lockout,
  password_minimum_length
)
VALUES (1, 'Compta Control', 'fr', 'DD/MM/YYYY', ',', ' ', 480, 5, 8)
ON CONFLICT (id) DO NOTHING;

COMMIT;
