use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};
use axum::{
    extract::{Path, State},
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::{DateTime, Utc};
use companytool_shared_protocol::{RecordSummary, ServerMessage};
use rand_core::RngCore;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use sqlx::{PgPool, Postgres, Row, Transaction};
use std::{env, path::PathBuf};
use uuid::Uuid;

use crate::{crypto_at_rest::DataCrypto, AppState};

#[derive(Debug, Deserialize)]
pub struct RegisterOrganizationRequest {
    pub organization_name: String,
    pub email: String,
    #[serde(default, alias = "terms_accepted")]
    pub accept_terms: bool,
}

#[derive(Debug, Deserialize)]
pub struct DevBootstrapLocalRequest {
    pub organization_name: String,
    pub email: String,
}

#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Deserialize)]
pub struct SelectOrganizationRequest {
    pub organization_id: Uuid,
}

#[derive(Debug, Deserialize)]
pub struct ChangeInitialPasswordRequest {
    pub email: String,
    pub current_password: String,
    pub new_password: String,
    pub new_password_confirm: String,
}

#[derive(Debug, Deserialize)]
pub struct RequestPasswordResetRequest {
    pub email: String,
}

#[derive(Debug, Deserialize)]
pub struct ResetPasswordRequest {
    pub token: String,
    pub new_password: String,
    pub new_password_confirm: String,
}

#[derive(Debug, Deserialize)]
pub struct AcceptInvitationRequest {
    pub token: String,
    pub new_password: String,
    pub new_password_confirm: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OrganizationSetupRequest {
    pub display_name: String,
    pub legal_form: Option<String>,
    pub street: String,
    pub postal_code: String,
    pub city: String,
    pub country: String,
    pub vat_id: Option<String>,
    pub email: String,
    pub phone: Option<String>,
    pub default_tax_rate: String,
    pub default_payment_days: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserNavigationSettings {
    pub mode: String,
}

#[derive(Debug, Deserialize)]
pub struct NumberRangeRequest {
    pub pattern: String,
    pub counter_value: i64,
    pub counter_padding: i32,
    pub reset_rule: Option<String>,
    pub is_active: bool,
}

#[derive(Debug, Serialize)]
pub struct NumberRangeResponse {
    pub id: Uuid,
    pub code: String,
    pub pattern: String,
    pub counter_value: i64,
    pub counter_padding: i32,
    pub reset_rule: Option<String>,
    pub is_active: bool,
}

#[derive(Debug, Serialize)]
pub struct NextNumberResponse {
    pub code: String,
    pub number: String,
}

#[derive(Debug, Deserialize)]
pub struct InviteUserRequest {
    pub email: String,
    #[serde(default)]
    pub roles: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRolesRequest {
    pub roles: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CustomerDetails {
    pub street: String,
    pub postal_code: String,
    pub city: String,
    pub country: String,
    pub email: String,
    pub phone: String,
}

#[derive(Debug, Deserialize)]
pub struct CustomerRequest {
    pub customer_number: String,
    pub name: String,
    pub status: String,
    pub details: CustomerDetails,
    pub standard_discount_percent: String,
    pub cash_discount_term_id: Option<Uuid>,
}

#[derive(Debug, Serialize)]
pub struct CustomerResponse {
    pub id: Uuid,
    pub customer_number: String,
    pub name: String,
    pub status: String,
    pub details: CustomerDetails,
    pub standard_discount_percent: String,
    pub cash_discount_term_id: Option<Uuid>,
}

#[derive(Debug, Deserialize)]
pub struct SupplierRequest {
    pub supplier_number: String,
    pub name: String,
    pub status: String,
    pub details: CustomerDetails,
    pub standard_discount_percent: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub payment_days: Option<i32>,
}

#[derive(Debug, Serialize)]
pub struct SupplierResponse {
    pub id: Uuid,
    pub supplier_number: String,
    pub name: String,
    pub status: String,
    pub details: CustomerDetails,
    pub standard_discount_percent: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub payment_days: Option<i32>,
}

#[derive(Debug, Deserialize)]
pub struct ItemRequest {
    pub item_number: String,
    pub name: String,
    #[serde(default)]
    pub manufacturer_code: Option<String>,
    pub unit: String,
    #[serde(deserialize_with = "deserialize_string_or_number")]
    pub tax_rate: String,
    #[serde(default, deserialize_with = "deserialize_optional_string_or_number")]
    pub default_purchase_price: Option<String>,
    #[serde(default, deserialize_with = "deserialize_optional_string_or_number")]
    pub default_sales_price: Option<String>,
    pub status: String,
    #[serde(default)]
    pub supplier_prices: Vec<ItemSupplierPriceRequest>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ItemSupplierPriceRequest {
    pub supplier_id: Uuid,
    pub external_item_number: String,
    #[serde(deserialize_with = "deserialize_string_or_number")]
    pub purchase_price: String,
    #[serde(default = "default_currency")]
    pub currency: String,
    #[serde(default)]
    pub is_preferred: bool,
    pub valid_from: Option<String>,
    pub valid_until: Option<String>,
}

fn default_currency() -> String {
    "EUR".to_string()
}

fn default_line_kind() -> String {
    "item".to_string()
}

fn deserialize_string_or_number<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    value_to_string(value).map_err(de::Error::custom)
}

fn deserialize_optional_string_or_number<'de, D>(
    deserializer: D,
) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
    value
        .map(value_to_string)
        .transpose()
        .map(|value| {
            value.and_then(|text| {
                if text.trim().is_empty() {
                    None
                } else {
                    Some(text)
                }
            })
        })
        .map_err(de::Error::custom)
}

fn value_to_string(value: serde_json::Value) -> Result<String, String> {
    match value {
        serde_json::Value::String(value) => Ok(value),
        serde_json::Value::Number(value) => Ok(value.to_string()),
        serde_json::Value::Null => Ok(String::new()),
        _ => Err("Wert muss Text oder Zahl sein".to_string()),
    }
}

#[derive(Debug, Serialize)]
pub struct ItemResponse {
    pub id: Uuid,
    pub item_number: String,
    pub name: String,
    pub manufacturer_code: Option<String>,
    pub unit: String,
    pub tax_rate: String,
    pub default_purchase_price: Option<String>,
    pub default_sales_price: Option<String>,
    pub status: String,
    pub supplier_prices: Vec<ItemSupplierPriceResponse>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ItemSupplierPriceResponse {
    pub id: Uuid,
    pub supplier_id: Uuid,
    pub supplier_number: String,
    pub supplier_name: String,
    pub external_item_number: String,
    pub purchase_price: String,
    pub currency: String,
    pub is_preferred: bool,
    pub valid_from: Option<String>,
    pub valid_until: Option<String>,
    pub source: String,
}

#[derive(Debug, Serialize)]
pub struct ItemPriceHistoryResponse {
    pub id: Uuid,
    pub item_id: Uuid,
    pub purchase_price: Option<String>,
    pub sales_price: Option<String>,
    pub source: String,
    pub valid_from: DateTime<Utc>,
    pub created_by_user_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CashDiscountTermRequest {
    pub code: String,
    pub name: String,
    pub discount_percent: String,
    pub discount_days: i32,
    pub net_days: Option<i32>,
    pub valid_from: Option<String>,
    pub valid_until: Option<String>,
    pub is_default_customer_term: bool,
    pub is_default_supplier_term: bool,
    pub is_active: bool,
}

#[derive(Debug, Serialize)]
pub struct CashDiscountTermResponse {
    pub id: Uuid,
    pub code: String,
    pub name: String,
    pub discount_percent: String,
    pub discount_days: i32,
    pub net_days: Option<i32>,
    pub valid_from: Option<String>,
    pub valid_until: Option<String>,
    pub is_default_customer_term: bool,
    pub is_default_supplier_term: bool,
    pub is_active: bool,
}

#[derive(Debug, Deserialize)]
pub struct QuoteItemRequest {
    #[serde(default = "default_line_kind")]
    pub line_kind: String,
    pub item_id: Option<Uuid>,
    pub activity_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub original_unit_price: Option<String>,
    pub discount_percent: String,
    pub tax_rate: String,
}

#[derive(Debug, Deserialize)]
pub struct QuoteRequest {
    pub quote_number: String,
    pub customer_id: Uuid,
    pub status: String,
    pub valid_until: Option<String>,
    pub cash_discount_term_id: Option<Uuid>,
    pub customer_discount_percent: String,
    pub notes: String,
    pub items: Vec<QuoteItemRequest>,
}

#[derive(Debug, Serialize)]
pub struct QuoteItemResponse {
    pub id: Uuid,
    pub line_number: i32,
    pub line_kind: String,
    pub item_id: Option<Uuid>,
    pub activity_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub original_unit_price: Option<String>,
    pub discount_percent: String,
    pub tax_rate: String,
    pub price_overridden: bool,
}

#[derive(Debug, Serialize)]
pub struct QuoteResponse {
    pub id: Uuid,
    pub quote_number: String,
    pub customer_id: Uuid,
    pub status: String,
    pub valid_until: Option<String>,
    pub cash_discount_term_id: Option<Uuid>,
    pub customer_discount_percent: String,
    pub notes: String,
    pub items: Vec<QuoteItemResponse>,
}

#[derive(Debug, Deserialize)]
pub struct OutgoingInvoiceItemRequest {
    #[serde(default = "default_line_kind")]
    pub line_kind: String,
    pub item_id: Option<Uuid>,
    pub activity_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub original_unit_price: Option<String>,
    pub discount_percent: String,
    pub tax_rate: String,
}

#[derive(Debug, Deserialize)]
pub struct OutgoingInvoiceRequest {
    pub invoice_number: String,
    pub customer_id: Uuid,
    pub status: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub customer_discount_percent: String,
    pub issued_at: Option<String>,
    pub due_at: Option<String>,
    pub source_quote_id: Option<Uuid>,
    pub items: Vec<OutgoingInvoiceItemRequest>,
}

#[derive(Debug, Serialize)]
pub struct OutgoingInvoiceItemResponse {
    pub id: Uuid,
    pub line_number: i32,
    pub line_kind: String,
    pub item_id: Option<Uuid>,
    pub activity_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub original_unit_price: Option<String>,
    pub discount_percent: String,
    pub tax_rate: String,
    pub price_overridden: bool,
}

#[derive(Debug, Serialize)]
pub struct OutgoingInvoiceResponse {
    pub id: Uuid,
    pub invoice_number: String,
    pub customer_id: Uuid,
    pub status: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub customer_discount_percent: String,
    pub issued_at: Option<String>,
    pub due_at: Option<String>,
    pub source_quote_id: Option<Uuid>,
    pub finalized_at: Option<DateTime<Utc>>,
    pub items: Vec<OutgoingInvoiceItemResponse>,
}

#[derive(Debug, Deserialize)]
pub struct IncomingInvoiceItemRequest {
    pub item_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub tax_rate: String,
}

#[derive(Debug, Deserialize)]
pub struct IncomingInvoiceRequest {
    pub invoice_number: String,
    pub supplier_id: Uuid,
    pub status: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub invoice_date: Option<String>,
    pub due_at: Option<String>,
    pub items: Vec<IncomingInvoiceItemRequest>,
}

#[derive(Debug, Serialize)]
pub struct IncomingInvoiceItemResponse {
    pub id: Uuid,
    pub line_number: i32,
    pub item_id: Option<Uuid>,
    pub description: String,
    pub quantity: String,
    pub unit_price: String,
    pub tax_rate: String,
}

#[derive(Debug, Serialize)]
pub struct IncomingInvoiceResponse {
    pub id: Uuid,
    pub invoice_number: String,
    pub supplier_id: Uuid,
    pub status: String,
    pub cash_discount_term_id: Option<Uuid>,
    pub invoice_date: Option<String>,
    pub due_at: Option<String>,
    pub items: Vec<IncomingInvoiceItemResponse>,
}

#[derive(Debug, Deserialize)]
pub struct PriceListImportRequest {
    pub source_name: String,
    pub content: String,
    pub delimiter: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct PriceListImportPreview {
    pub rows: Vec<PriceListImportRow>,
    pub total_rows: usize,
    pub valid_rows: usize,
    pub error_rows: usize,
}

#[derive(Debug, Clone, Serialize)]
pub struct PriceListImportRow {
    pub row_number: usize,
    pub item_number: String,
    pub name: String,
    pub manufacturer_code: Option<String>,
    pub unit: String,
    pub tax_rate: String,
    pub purchase_price: Option<String>,
    pub sales_price: Option<String>,
    pub supplier_number: Option<String>,
    pub supplier_item_number: Option<String>,
    pub currency: Option<String>,
    pub action: String,
    pub error: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct PriceListImportApplyResponse {
    pub import_id: Uuid,
    pub applied_rows: usize,
    pub error_rows: usize,
}

#[derive(Debug, Deserialize)]
pub struct ApiConnectorRequest {
    pub code: String,
    pub name: String,
    pub connector_type: String,
    pub config: serde_json::Value,
    pub is_active: bool,
    pub sync_interval_minutes: Option<i32>,
}

#[derive(Debug, Serialize)]
pub struct ApiConnectorResponse {
    pub id: Uuid,
    pub code: String,
    pub name: String,
    pub connector_type: String,
    pub config: serde_json::Value,
    pub is_active: bool,
    pub sync_interval_minutes: Option<i32>,
    pub last_sync_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Deserialize)]
pub struct PriceRuleRequest {
    pub code: String,
    pub name: String,
    pub source_type: String,
    pub source_id: Option<Uuid>,
    pub markup_percent: String,
    pub rounding_mode: String,
    pub is_active: bool,
}

#[derive(Debug, Serialize)]
pub struct PriceRuleResponse {
    pub id: Uuid,
    pub code: String,
    pub name: String,
    pub source_type: String,
    pub source_id: Option<Uuid>,
    pub markup_percent: String,
    pub rounding_mode: String,
    pub is_active: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EntityLinkRequest {
    pub entity_type: String,
    pub entity_id: Uuid,
}

#[derive(Debug, Deserialize)]
pub struct CommunicationRequest {
    pub communication_type: String,
    pub direction: String,
    pub subject: String,
    pub body: String,
    pub status: String,
    pub occurred_at: Option<String>,
    #[serde(default)]
    pub links: Vec<EntityLinkRequest>,
}

#[derive(Debug, Serialize)]
pub struct CommunicationResponse {
    pub id: Uuid,
    pub communication_type: String,
    pub direction: String,
    pub subject: String,
    pub body: String,
    pub status: String,
    pub occurred_at: Option<DateTime<Utc>>,
    pub links: Vec<EntityLinkRequest>,
}

#[derive(Debug, Deserialize)]
pub struct DocumentUploadRequest {
    pub title: String,
    pub description: String,
    pub file_name: String,
    pub content_type: String,
    pub content_base64: String,
    #[serde(default)]
    pub links: Vec<EntityLinkRequest>,
}

#[derive(Debug, Serialize)]
pub struct DocumentVersionResponse {
    pub id: Uuid,
    pub version_no: i32,
    pub file_name: String,
    pub content_type: String,
    pub file_size: i64,
    pub checksum_sha256: String,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct DocumentResponse {
    pub id: Uuid,
    pub title: String,
    pub description: String,
    pub status: String,
    pub latest_version: Option<DocumentVersionResponse>,
    pub links: Vec<EntityLinkRequest>,
}

#[derive(Debug, Serialize)]
pub struct DocumentDownloadResponse {
    pub document_id: Uuid,
    pub version_id: Uuid,
    pub file_name: String,
    pub content_type: String,
    pub content_base64: String,
}

#[derive(Debug, Serialize)]
pub struct DocumentAuditLogResponse {
    pub id: Uuid,
    pub document_id: Uuid,
    pub version_id: Option<Uuid>,
    pub action: String,
    pub user_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct ActivityRequest {
    #[serde(default)]
    pub activity_number: Option<String>,
    pub activity_type: String,
    pub title: String,
    pub body: String,
    pub status: String,
    pub priority: String,
    pub due_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub struct ActivityResponse {
    pub id: Uuid,
    pub activity_number: Option<String>,
    pub activity_type: String,
    pub title: String,
    pub body: String,
    pub status: String,
    pub priority: String,
    pub due_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub struct OrganizationRegistrationListItem {
    pub id: Uuid,
    pub organization_name: String,
    pub email: String,
    pub status: String,
    pub requested_at: DateTime<Utc>,
    pub decided_at: Option<DateTime<Utc>>,
    pub decided_by_user_id: Option<Uuid>,
}

#[derive(Debug, Serialize)]
pub struct OrganizationRegistrationDetail {
    pub id: Uuid,
    pub organization_name: String,
    pub email: String,
    pub status: String,
    pub organization_id: Option<Uuid>,
    pub schema_name: Option<String>,
    pub requested_at: DateTime<Utc>,
    pub decided_at: Option<DateTime<Utc>>,
    pub decided_by_user_id: Option<Uuid>,
    pub decision_note: Option<String>,
    pub provisioning_error: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct OrganizationUserResponse {
    pub user_id: Uuid,
    pub email: String,
    pub status: String,
    pub roles: Vec<String>,
}

pub async fn dev_bootstrap_local(
    State(state): State<AppState>,
    Json(payload): Json<DevBootstrapLocalRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    if !dev_mode_enabled() {
        return Err(ApiError::forbidden("Dev-Bootstrap ist nicht aktiv"));
    }

    let db = state.db()?;
    let organization_name = payload.organization_name.trim();
    let email = normalize_email(&payload.email)?;

    if organization_name.len() < 2 {
        return Err(ApiError::bad_request("Firmenname ist zu kurz"));
    }

    let user_id = ensure_user(db, &email).await?;
    let organization_id = Uuid::new_v4();
    let schema_name = company_schema_name(organization_id);
    let encrypted_name = state.crypto.encrypt(&organization_name.to_string())?;
    let initial_password = generate_initial_password();
    let password_hash = hash_password(&initial_password)?;

    let mut tx = db.begin().await?;
    sqlx::query(
        r#"
        insert into organizations (
            id, display_name_ciphertext, display_name_nonce, display_name_key_id,
            schema_name, status, registration_email, approved_at, setup_completed_at
        ) values ($1, $2, $3, $4, $5, 'active', $6, now(), now())
        "#,
    )
    .bind(organization_id)
    .bind(encrypted_name.ciphertext)
    .bind(encrypted_name.nonce)
    .bind(encrypted_name.key_id)
    .bind(&schema_name)
    .bind(&email)
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        r#"
        insert into user_organizations (user_id, organization_id, status, accepted_at)
        values ($1, $2, 'active', now())
        on conflict (user_id, organization_id) do update set
            status = 'active',
            accepted_at = coalesce(user_organizations.accepted_at, now()),
            updated_at = now()
        "#,
    )
    .bind(user_id)
    .bind(organization_id)
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        r#"
        update users
        set password_hash = $2,
            must_change_password = false,
            initial_password_expires_at = null,
            updated_at = now()
        where id = $1
        "#,
    )
    .bind(user_id)
    .bind(password_hash)
    .execute(&mut *tx)
    .await?;

    provision_company_schema_tx(&mut tx, &schema_name).await?;
    assign_all_roles_tx(&mut tx, &schema_name, user_id).await?;
    tx.commit().await?;

    Ok(Json(json!({
        "organization_id": organization_id,
        "schema_name": schema_name,
        "user_id": user_id,
        "email": email,
        "password": initial_password,
        "dev_mode": true
    })))
}

pub async fn register_organization(
    State(state): State<AppState>,
    Json(payload): Json<RegisterOrganizationRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let organization_name = payload.organization_name.trim();
    let email = normalize_email(&payload.email)?;

    if organization_name.len() < 2 {
        return Err(ApiError::bad_request("Firmenname ist zu kurz"));
    }

    if !payload.accept_terms {
        return Err(ApiError::bad_request(
            "Nutzungsbedingungen müssen akzeptiert werden",
        ));
    }

    let existing_open: Option<Uuid> = sqlx::query_scalar(
        "select id from organization_registration_requests where email = $1 and status = 'pending_approval' limit 1",
    )
    .bind(&email)
    .fetch_optional(db)
    .await?;

    if existing_open.is_some() {
        return Err(ApiError::conflict(
            "Es gibt bereits eine offene Registrierung",
        ));
    }

    let user_id = ensure_user(db, &email).await?;
    let encrypted_name = state.crypto.encrypt(&organization_name.to_string())?;
    let registration_id = Uuid::new_v4();

    sqlx::query(
        r#"
        insert into organization_registration_requests (
            id,
            organization_name_ciphertext,
            organization_name_nonce,
            organization_name_key_id,
            email,
            terms_accepted_at
        ) values ($1, $2, $3, $4, $5, now())
        "#,
    )
    .bind(registration_id)
    .bind(encrypted_name.ciphertext)
    .bind(encrypted_name.nonce)
    .bind(encrypted_name.key_id)
    .bind(&email)
    .execute(db)
    .await?;

    Ok(Json(json!({
        "id": registration_id,
        "user_id": user_id,
        "status": "pending_approval"
    })))
}

pub async fn list_organization_registrations(
    State(state): State<AppState>,
) -> Result<Json<Vec<OrganizationRegistrationListItem>>, ApiError> {
    let db = state.db()?;
    let rows = sqlx::query(
        r#"
        select id, organization_name_ciphertext, organization_name_nonce, organization_name_key_id,
               email, status, requested_at, decided_at, decided_by_user_id
        from organization_registration_requests
        order by requested_at desc
        "#,
    )
    .fetch_all(db)
    .await?;

    let mut items = Vec::with_capacity(rows.len());
    for row in rows {
        items.push(OrganizationRegistrationListItem {
            id: row.get("id"),
            organization_name: decrypt_string(
                &state.crypto,
                row.get("organization_name_ciphertext"),
                row.get("organization_name_nonce"),
                row.get("organization_name_key_id"),
            )?,
            email: row.get("email"),
            status: row.get("status"),
            requested_at: row.get("requested_at"),
            decided_at: row.get("decided_at"),
            decided_by_user_id: row.get("decided_by_user_id"),
        });
    }

    Ok(Json(items))
}

pub async fn get_organization_registration(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<OrganizationRegistrationDetail>, ApiError> {
    Ok(Json(load_registration_detail(&state, id).await?))
}

pub async fn approve_organization_registration(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let row = load_registration_row(db, id).await?;
    let email: String = row.get("email");
    let organization_name = decrypt_string(
        &state.crypto,
        row.get("organization_name_ciphertext"),
        row.get("organization_name_nonce"),
        row.get("organization_name_key_id"),
    )?;

    let user_id = ensure_user(db, &email).await?;
    let organization_id = row
        .try_get::<Option<Uuid>, _>("organization_id")
        .ok()
        .flatten()
        .unwrap_or_else(Uuid::new_v4);
    let schema_name = company_schema_name(organization_id);
    let encrypted_name = state.crypto.encrypt(&organization_name)?;

    let mut tx = db.begin().await?;
    sqlx::query(
        r#"
        insert into organizations (
            id, display_name_ciphertext, display_name_nonce, display_name_key_id,
            schema_name, status, registration_email, approved_at
        ) values ($1, $2, $3, $4, $5, 'active', $6, now())
        on conflict (id) do update set
            status = 'active',
            schema_name = excluded.schema_name,
            approved_at = coalesce(organizations.approved_at, now()),
            updated_at = now()
        "#,
    )
    .bind(organization_id)
    .bind(encrypted_name.ciphertext)
    .bind(encrypted_name.nonce)
    .bind(encrypted_name.key_id)
    .bind(&schema_name)
    .bind(&email)
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        r#"
        insert into user_organizations (user_id, organization_id, status, accepted_at)
        values ($1, $2, 'active', now())
        on conflict (user_id, organization_id) do update set
            status = 'active',
            accepted_at = coalesce(user_organizations.accepted_at, now()),
            updated_at = now()
        "#,
    )
    .bind(user_id)
    .bind(organization_id)
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        r#"
        update organization_registration_requests
        set status = 'active', organization_id = $2, decided_at = now()
        where id = $1
        "#,
    )
    .bind(id)
    .bind(organization_id)
    .execute(&mut *tx)
    .await?;

    let initial_password = generate_initial_password();
    let password_hash = hash_password(&initial_password)?;
    sqlx::query(
        r#"
        update users
        set password_hash = $2,
            must_change_password = true,
            initial_password_expires_at = now() + interval '7 days',
            updated_at = now()
        where id = $1
        "#,
    )
    .bind(user_id)
    .bind(password_hash)
    .execute(&mut *tx)
    .await?;

    provision_company_schema_tx(&mut tx, &schema_name).await?;
    assign_all_roles_tx(&mut tx, &schema_name, user_id).await?;
    tx.commit().await?;
    enqueue_initial_password_email(&state.crypto, db, &email, &initial_password).await?;
    emit_change(&state, "Benutzer eingeladen");

    let mut response = json!({
        "id": id,
        "organization_id": organization_id,
        "schema_name": schema_name,
        "status": "active"
    });
    if dev_mode_enabled() {
        response["dev_initial_password"] = json!(initial_password);
    }
    Ok(Json(response))
}

pub async fn reject_organization_registration(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    sqlx::query(
        "update organization_registration_requests set status = 'rejected', decided_at = now() where id = $1",
    )
    .bind(id)
    .execute(db)
    .await?;

    Ok(Json(json!({ "id": id, "status": "rejected" })))
}

pub async fn resend_initial_email(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let detail = load_registration_detail(&state, id).await?;
    let password = generate_initial_password();
    let user_id = ensure_user(db, &detail.email).await?;
    let password_hash = hash_password(&password)?;

    sqlx::query(
        r#"
        update users
        set password_hash = $2,
            must_change_password = true,
            initial_password_expires_at = now() + interval '7 days',
            updated_at = now()
        where id = $1
        "#,
    )
    .bind(user_id)
    .bind(password_hash)
    .execute(db)
    .await?;
    enqueue_initial_password_email(&state.crypto, db, &detail.email, &password).await?;

    let mut response = json!({ "queued": true });
    if dev_mode_enabled() {
        response["dev_initial_password"] = json!(password);
    }
    Ok(Json(response))
}

pub async fn retry_provisioning(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let detail = load_registration_detail(&state, id).await?;
    let organization_id = detail
        .organization_id
        .ok_or_else(|| ApiError::bad_request("Registrierung ist noch nicht freigeschaltet"))?;
    let schema_name = detail
        .schema_name
        .unwrap_or_else(|| company_schema_name(organization_id));
    provision_company_schema(db, &schema_name).await?;

    Ok(Json(
        json!({ "schema_name": schema_name, "provisioned": true }),
    ))
}

pub async fn login(
    State(state): State<AppState>,
    Json(payload): Json<LoginRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let email = normalize_email(&payload.email)?;
    let row = sqlx::query(
        "select id, password_hash, must_change_password from users where email = $1 and is_active = true",
    )
    .bind(&email)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::unauthorized("Login fehlgeschlagen"))?;

    let password_hash: Option<String> = row.get("password_hash");
    let Some(password_hash) = password_hash else {
        return Err(ApiError::unauthorized("Login fehlgeschlagen"));
    };
    verify_password(&payload.password, &password_hash)?;

    let user_id: Uuid = row.get("id");
    sqlx::query("update users set last_login_at = now(), updated_at = now() where id = $1")
        .bind(user_id)
        .execute(db)
        .await?;

    let orgs = sqlx::query(
        r#"
        select o.id, o.schema_name, o.status
        from organizations o
        join user_organizations uo on uo.organization_id = o.id
        where uo.user_id = $1 and uo.status = 'active'
        order by o.created_at
        "#,
    )
    .bind(user_id)
    .fetch_all(db)
    .await?;

    let organizations = orgs
        .into_iter()
        .map(|row| {
            json!({
                "id": row.get::<Uuid, _>("id"),
                "schema_name": row.get::<Option<String>, _>("schema_name"),
                "status": row.get::<String, _>("status")
            })
        })
        .collect::<Vec<_>>();
    let selected_organization_id = organizations
        .first()
        .and_then(|organization| organization.get("id"))
        .and_then(|value| serde_json::from_value::<Uuid>(value.clone()).ok());
    let access_token = create_session_token(db, user_id, selected_organization_id).await?;

    Ok(Json(json!({
        "user_id": user_id,
        "access_token": access_token,
        "organization_id": selected_organization_id,
        "must_change_password": row.get::<bool, _>("must_change_password"),
        "organizations": organizations
    })))
}

pub async fn change_initial_password(
    State(state): State<AppState>,
    Json(payload): Json<ChangeInitialPasswordRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    validate_new_password(&payload.new_password, &payload.new_password_confirm)?;

    let db = state.db()?;
    let email = normalize_email(&payload.email)?;
    let row =
        sqlx::query("select id, password_hash from users where email = $1 and is_active = true")
            .bind(&email)
            .fetch_optional(db)
            .await?
            .ok_or_else(|| ApiError::unauthorized("Login fehlgeschlagen"))?;
    let password_hash: Option<String> = row.get("password_hash");
    verify_password(
        &payload.current_password,
        password_hash.as_deref().unwrap_or_default(),
    )?;
    let new_hash = hash_password(&payload.new_password)?;

    sqlx::query(
        r#"
        update users
        set password_hash = $2,
            must_change_password = false,
            initial_password_expires_at = null,
            updated_at = now()
        where id = $1
        "#,
    )
    .bind(row.get::<Uuid, _>("id"))
    .bind(new_hash)
    .execute(db)
    .await?;

    Ok(Json(json!({ "changed": true })))
}

pub async fn request_password_reset(
    State(state): State<AppState>,
    Json(payload): Json<RequestPasswordResetRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let email = normalize_email(&payload.email)?;
    let user_id =
        sqlx::query_scalar::<_, Uuid>("select id from users where email=$1 and is_active=true")
            .bind(&email)
            .fetch_optional(db)
            .await?;

    let mut dev_reset_token = None;
    if let Some(user_id) = user_id {
        let token = generate_token();
        let token_hash = hash_token(&token);
        sqlx::query(
            r#"
            insert into password_reset_tokens (id, user_id, token_hash, expires_at)
            values ($1,$2,$3,now() + interval '30 minutes')
            "#,
        )
        .bind(Uuid::new_v4())
        .bind(user_id)
        .bind(&token_hash)
        .execute(db)
        .await?;
        enqueue_password_reset_email(&state.crypto, db, &email, &token).await?;
        if dev_mode_enabled() {
            dev_reset_token = Some(token);
        }
    }

    let mut response = json!({ "queued": true });
    if let Some(token) = dev_reset_token {
        response["dev_reset_token"] = json!(token);
    }
    Ok(Json(response))
}

pub async fn reset_password(
    State(state): State<AppState>,
    Json(payload): Json<ResetPasswordRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    validate_new_password(&payload.new_password, &payload.new_password_confirm)?;
    let db = state.db()?;
    let token_hash = hash_token(&payload.token);
    let row = sqlx::query(
        r#"
        select id, user_id
        from password_reset_tokens
        where token_hash=$1 and used_at is null and expires_at > now()
        "#,
    )
    .bind(&token_hash)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::bad_request("Reset-Token ist ungültig oder abgelaufen"))?;
    let reset_id: Uuid = row.get("id");
    let user_id: Uuid = row.get("user_id");
    let new_hash = hash_password(&payload.new_password)?;
    let mut tx = db.begin().await?;
    sqlx::query(
        r#"
        update users
        set password_hash=$2, must_change_password=false,
            initial_password_expires_at=null, updated_at=now()
        where id=$1
        "#,
    )
    .bind(user_id)
    .bind(new_hash)
    .execute(&mut *tx)
    .await?;
    sqlx::query("update password_reset_tokens set used_at=now() where id=$1")
        .bind(reset_id)
        .execute(&mut *tx)
        .await?;
    sqlx::query(
        "update refresh_tokens set revoked_at=now(), revoked_reason='password_reset' where user_id=$1 and revoked_at is null",
    )
    .bind(user_id)
    .execute(&mut *tx)
    .await?;
    tx.commit().await?;
    Ok(Json(json!({ "changed": true })))
}

pub async fn accept_invitation(
    State(state): State<AppState>,
    Json(payload): Json<AcceptInvitationRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    validate_new_password(&payload.new_password, &payload.new_password_confirm)?;
    let db = state.db()?;
    let token_hash = hash_token(&payload.token);
    let row = sqlx::query(
        r#"
        select id, organization_id, email, created_user_id
        from user_invitations
        where token_hash=$1 and status='pending' and expires_at > now()
        "#,
    )
    .bind(&token_hash)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::bad_request("Einladung ist ungültig oder abgelaufen"))?;
    let invitation_id: Uuid = row.get("id");
    let organization_id: Uuid = row.get("organization_id");
    let email: String = row.get("email");
    let user_id: Uuid = row
        .get::<Option<Uuid>, _>("created_user_id")
        .unwrap_or_else(Uuid::new_v4);
    let password_hash = hash_password(&payload.new_password)?;

    let mut tx = db.begin().await?;
    sqlx::query(
        r#"
        update users
        set password_hash=$2, must_change_password=false,
            initial_password_expires_at=null, updated_at=now()
        where id=$1
        "#,
    )
    .bind(user_id)
    .bind(password_hash)
    .execute(&mut *tx)
    .await?;
    sqlx::query(
        r#"
        update user_organizations
        set status='active', accepted_at=now(), updated_at=now()
        where user_id=$1 and organization_id=$2
        "#,
    )
    .bind(user_id)
    .bind(organization_id)
    .execute(&mut *tx)
    .await?;
    sqlx::query(
        r#"
        update user_invitations
        set status='accepted', accepted_at=now(), accepted_by_user_id=$2
        where id=$1
        "#,
    )
    .bind(invitation_id)
    .bind(user_id)
    .execute(&mut *tx)
    .await?;
    tx.commit().await?;
    Ok(Json(json!({
        "accepted": true,
        "email": email,
        "organization_id": organization_id
    })))
}

pub async fn auth_organizations(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
    let db = state.db()?;
    let auth = require_auth(db, &headers).await?;
    let rows = sqlx::query(
        r#"
        select o.id, o.schema_name, o.status
        from organizations o
        join user_organizations uo on uo.organization_id = o.id
        where uo.user_id = $1 and uo.status = 'active'
        order by o.created_at
        "#,
    )
    .bind(auth.user_id)
    .fetch_all(db)
    .await?;
    Ok(Json(
        rows.into_iter()
            .map(|row| {
                json!({
                    "id": row.get::<Uuid, _>("id"),
                    "schema_name": row.get::<Option<String>, _>("schema_name"),
                    "status": row.get::<String, _>("status")
                })
            })
            .collect(),
    ))
}

pub async fn select_organization(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<SelectOrganizationRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let auth = require_auth(db, &headers).await?;
    let context =
        load_context_for_user_and_organization(db, auth.user_id, payload.organization_id).await?;
    update_session_organization(db, &headers, context.organization_id).await?;
    Ok(Json(json!({
        "selected": true,
        "organization_id": context.organization_id,
        "schema_name": context.schema_name
    })))
}

pub async fn get_organization_setup(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "settings.read").await?;
    let sql = format!(
        r#"
        select value_ciphertext, value_nonce, value_key_id
        from {schema}.settings
        where key = 'organization_setup'
        "#,
        schema = context.schema_name
    );
    let setup = match sqlx::query(&sql).fetch_optional(db).await? {
        Some(row) => Some(state.crypto.decrypt::<OrganizationSetupRequest>(
            row.get("value_ciphertext"),
            row.get("value_nonce"),
            row.get("value_key_id"),
        )?),
        None => None,
    };

    Ok(Json(json!({
        "organization_id": context.organization_id,
        "schema_name": context.schema_name,
        "setup": setup
    })))
}

pub async fn put_organization_setup(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<OrganizationSetupRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "settings.write").await?;
    let organization_id = context.organization_id;
    let encrypted_name = state.crypto.encrypt(&payload.display_name)?;
    let encrypted_settings = state.crypto.encrypt(&payload)?;

    sqlx::query(
        r#"
        update organizations
        set display_name_ciphertext = $2,
            display_name_nonce = $3,
            display_name_key_id = $4,
            setup_completed_at = coalesce(setup_completed_at, now()),
            updated_at = now()
        where id = $1
        "#,
    )
    .bind(organization_id)
    .bind(encrypted_name.ciphertext)
    .bind(encrypted_name.nonce)
    .bind(encrypted_name.key_id)
    .execute(db)
    .await?;

    let schema_name = context.schema_name;
    let sql = format!(
        r#"
        insert into {schema}.settings (key, value_ciphertext, value_nonce, value_key_id)
        values ('organization_setup', $1, $2, $3)
        on conflict (key) do update set
            value_ciphertext = excluded.value_ciphertext,
            value_nonce = excluded.value_nonce,
            value_key_id = excluded.value_key_id,
            updated_at = now()
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(encrypted_settings.ciphertext)
        .bind(encrypted_settings.nonce)
        .bind(encrypted_settings.key_id)
        .execute(db)
        .await?;

    emit_change(&state, "Firmendaten geändert");
    Ok(Json(json!({ "saved": true })))
}

pub async fn get_user_navigation_settings(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<UserNavigationSettings>, ApiError> {
    let db = state.db()?;
    let context = require_auth(db, &headers).await?;
    ensure_safe_schema_name(&context.schema_name)?;
    let sql = format!(
        r#"
        select value_ciphertext, value_nonce, value_key_id
        from {schema}.user_settings
        where user_id = $1 and key = 'navigation'
        "#,
        schema = context.schema_name
    );
    let settings = match sqlx::query(&sql)
        .bind(context.user_id)
        .fetch_optional(db)
        .await?
    {
        Some(row) => state.crypto.decrypt::<UserNavigationSettings>(
            row.get("value_ciphertext"),
            row.get("value_nonce"),
            row.get("value_key_id"),
        )?,
        None => default_user_navigation_settings(),
    };
    Ok(Json(normalize_user_navigation_settings(settings)?))
}

pub async fn put_user_navigation_settings(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<UserNavigationSettings>,
) -> Result<Json<UserNavigationSettings>, ApiError> {
    let db = state.db()?;
    let context = require_auth(db, &headers).await?;
    ensure_safe_schema_name(&context.schema_name)?;
    let settings = normalize_user_navigation_settings(payload)?;
    let encrypted = state.crypto.encrypt(&settings)?;
    let sql = format!(
        r#"
        insert into {schema}.user_settings (user_id, key, value_ciphertext, value_nonce, value_key_id)
        values ($1, 'navigation', $2, $3, $4)
        on conflict (user_id, key) do update set
            value_ciphertext = excluded.value_ciphertext,
            value_nonce = excluded.value_nonce,
            value_key_id = excluded.value_key_id,
            updated_at = now()
        "#,
        schema = context.schema_name
    );
    sqlx::query(&sql)
        .bind(context.user_id)
        .bind(encrypted.ciphertext)
        .bind(encrypted.nonce)
        .bind(encrypted.key_id)
        .execute(db)
        .await?;
    Ok(Json(settings))
}

pub async fn list_number_ranges(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<NumberRangeResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "number_ranges.read").await?;
    let sql = format!(
        r#"
        select id, code, pattern, counter_value, counter_padding, reset_rule, is_active
        from {schema}.number_ranges
        order by code
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    Ok(Json(rows.into_iter().map(number_range_from_row).collect()))
}

pub async fn update_number_range(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(code): Path<String>,
    Json(payload): Json<NumberRangeRequest>,
) -> Result<Json<NumberRangeResponse>, ApiError> {
    validate_number_range(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "number_ranges.write").await?;
    let sql = format!(
        r#"
        update {schema}.number_ranges
        set pattern=$2, counter_value=$3, counter_padding=$4,
            reset_rule=$5, is_active=$6, updated_at=now()
        where code=$1
        returning id, code, pattern, counter_value, counter_padding, reset_rule, is_active
        "#,
        schema = context.schema_name
    );
    let row = sqlx::query(&sql)
        .bind(code)
        .bind(payload.pattern.trim())
        .bind(payload.counter_value)
        .bind(payload.counter_padding)
        .bind(
            payload
                .reset_rule
                .as_deref()
                .filter(|value| !value.trim().is_empty()),
        )
        .bind(payload.is_active)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Nummernkreis nicht gefunden"))?;
    emit_change(&state, "Nummernkreis geändert");
    Ok(Json(number_range_from_row(row)))
}

pub async fn generate_next_number(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(code): Path<String>,
) -> Result<Json<NextNumberResponse>, ApiError> {
    ensure_known_number_range_code(&code)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, number_range_write_permission(&code)).await?;
    let number = next_number(db, &context.schema_name, &code).await?;
    Ok(Json(NextNumberResponse { code, number }))
}

pub async fn list_customers(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<CustomerResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "customers.read").await?;
    let sql = format!(
        r#"
        select c.id, c.customer_number, c.name_ciphertext, c.name_nonce, c.name_key_id,
               c.status, c.details_ciphertext, c.details_nonce, c.details_key_id,
               coalesce(terms.standard_discount_percent, 0)::text standard_discount_percent,
               terms.cash_discount_term_id
        from {schema}.customers c
        left join lateral (
            select standard_discount_percent, cash_discount_term_id
            from {schema}.customer_price_terms
            where customer_id = c.id and is_active = true
            order by updated_at desc
            limit 1
        ) terms on true
        order by c.updated_at desc, c.customer_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut customers = Vec::with_capacity(rows.len());
    for row in rows {
        customers.push(customer_from_row(&state.crypto, row)?);
    }
    Ok(Json(customers))
}

pub async fn create_customer(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<CustomerRequest>,
) -> Result<Json<CustomerResponse>, ApiError> {
    let mut payload = payload;
    validate_customer_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "customers.write").await?;
    payload.customer_number = next_number_if_blank(
        db,
        &context.schema_name,
        "customers",
        &payload.customer_number,
    )
    .await?;
    let customer_id = Uuid::new_v4();
    let encrypted_name = state.crypto.encrypt(&payload.name.trim().to_string())?;
    let encrypted_details = state.crypto.encrypt(&payload.details)?;
    let sql = format!(
        r#"
        insert into {schema}.customers (
            id, customer_number, name_ciphertext, name_nonce, name_key_id, status,
            details_ciphertext, details_nonce, details_key_id
        ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
        "#,
        schema = context.schema_name
    );
    sqlx::query(&sql)
        .bind(customer_id)
        .bind(payload.customer_number.trim())
        .bind(encrypted_name.ciphertext)
        .bind(encrypted_name.nonce)
        .bind(encrypted_name.key_id)
        .bind(&payload.status)
        .bind(encrypted_details.ciphertext)
        .bind(encrypted_details.nonce)
        .bind(encrypted_details.key_id)
        .execute(db)
        .await?;
    save_customer_terms(db, &context.schema_name, customer_id, &payload).await?;
    emit_change(&state, "Kunde angelegt");
    Ok(Json(customer_response(customer_id, payload)))
}

pub async fn update_customer(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(customer_id): Path<Uuid>,
    Json(payload): Json<CustomerRequest>,
) -> Result<Json<CustomerResponse>, ApiError> {
    validate_customer_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "customers.write").await?;
    let encrypted_name = state.crypto.encrypt(&payload.name.trim().to_string())?;
    let encrypted_details = state.crypto.encrypt(&payload.details)?;
    let sql = format!(
        r#"
        update {schema}.customers
        set customer_number = customer_number, name_ciphertext = $3, name_nonce = $4,
            name_key_id = $5, status = $6, details_ciphertext = $7,
            details_nonce = $8, details_key_id = $9, updated_at = now()
        where id = $1
        "#,
        schema = context.schema_name
    );
    let result = sqlx::query(&sql)
        .bind(customer_id)
        .bind(payload.customer_number.trim())
        .bind(encrypted_name.ciphertext)
        .bind(encrypted_name.nonce)
        .bind(encrypted_name.key_id)
        .bind(&payload.status)
        .bind(encrypted_details.ciphertext)
        .bind(encrypted_details.nonce)
        .bind(encrypted_details.key_id)
        .execute(db)
        .await?;
    if result.rows_affected() == 0 {
        return Err(ApiError::not_found("Kunde nicht gefunden"));
    }
    save_customer_terms(db, &context.schema_name, customer_id, &payload).await?;
    emit_change(&state, "Kunde geändert");
    Ok(Json(customer_response(customer_id, payload)))
}

pub async fn delete_customer(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(customer_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "customers.delete").await?;
    let sql = format!(
        "update {schema}.customers set status = 'inactive', updated_at = now() where id = $1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(customer_id).execute(db).await?;
    if result.rows_affected() == 0 {
        return Err(ApiError::not_found("Kunde nicht gefunden"));
    }
    emit_change(&state, "Kunde deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": customer_id })))
}

pub async fn list_suppliers(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<SupplierResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "suppliers.read").await?;
    let sql = format!(
        r#"
        select s.id, s.supplier_number, s.name_ciphertext, s.name_nonce, s.name_key_id,
               s.status, s.details_ciphertext, s.details_nonce, s.details_key_id,
               coalesce(terms.standard_discount_percent, 0)::text standard_discount_percent,
               terms.cash_discount_term_id, terms.payment_days
        from {schema}.suppliers s
        left join lateral (
            select standard_discount_percent, cash_discount_term_id, payment_days
            from {schema}.supplier_price_terms
            where supplier_id = s.id and is_active = true
            order by updated_at desc limit 1
        ) terms on true
        order by s.updated_at desc, s.supplier_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut suppliers = Vec::with_capacity(rows.len());
    for row in rows {
        suppliers.push(supplier_from_row(&state.crypto, row)?);
    }
    Ok(Json(suppliers))
}

pub async fn create_supplier(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<SupplierRequest>,
) -> Result<Json<SupplierResponse>, ApiError> {
    validate_supplier_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "suppliers.write").await?;
    let supplier_id = Uuid::new_v4();
    let mut payload = payload;
    payload.supplier_number = next_number_if_blank(
        db,
        &context.schema_name,
        "suppliers",
        &payload.supplier_number,
    )
    .await?;
    write_supplier(
        db,
        &state.crypto,
        &context.schema_name,
        supplier_id,
        &payload,
        true,
    )
    .await?;
    save_supplier_terms(db, &context.schema_name, supplier_id, &payload).await?;
    emit_change(&state, "Lieferant angelegt");
    Ok(Json(supplier_response(supplier_id, payload)))
}

pub async fn update_supplier(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(supplier_id): Path<Uuid>,
    Json(payload): Json<SupplierRequest>,
) -> Result<Json<SupplierResponse>, ApiError> {
    validate_supplier_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "suppliers.write").await?;
    write_supplier(
        db,
        &state.crypto,
        &context.schema_name,
        supplier_id,
        &payload,
        false,
    )
    .await?;
    save_supplier_terms(db, &context.schema_name, supplier_id, &payload).await?;
    emit_change(&state, "Lieferant geändert");
    Ok(Json(supplier_response(supplier_id, payload)))
}

pub async fn delete_supplier(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(supplier_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "suppliers.delete").await?;
    set_record_inactive(db, &context.schema_name, "suppliers", supplier_id).await?;
    emit_change(&state, "Lieferant deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": supplier_id })))
}

pub async fn list_cash_discount_terms(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<CashDiscountTermResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "cash_discount_terms.read").await?;
    let sql = format!(
        r#"
        select id, code, name, discount_percent::text discount_percent,
               discount_days, net_days, valid_from::text valid_from,
               valid_until::text valid_until, is_default_customer_term,
               is_default_supplier_term, is_active
        from {schema}.cash_discount_terms
        order by is_active desc, code
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    Ok(Json(
        rows.into_iter().map(cash_discount_term_from_row).collect(),
    ))
}

pub async fn create_cash_discount_term(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<CashDiscountTermRequest>,
) -> Result<Json<CashDiscountTermResponse>, ApiError> {
    validate_cash_discount_term(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "cash_discount_terms.write").await?;
    let term_id = Uuid::new_v4();
    write_cash_discount_term(db, &context.schema_name, term_id, &payload, true).await?;
    emit_change(&state, "Skonto-Regel angelegt");
    Ok(Json(cash_discount_term_response(term_id, payload)))
}

pub async fn update_cash_discount_term(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(term_id): Path<Uuid>,
    Json(payload): Json<CashDiscountTermRequest>,
) -> Result<Json<CashDiscountTermResponse>, ApiError> {
    validate_cash_discount_term(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "cash_discount_terms.write").await?;
    write_cash_discount_term(db, &context.schema_name, term_id, &payload, false).await?;
    emit_change(&state, "Skonto-Regel geändert");
    Ok(Json(cash_discount_term_response(term_id, payload)))
}

pub async fn delete_cash_discount_term(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(term_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "cash_discount_terms.delete").await?;
    let sql = format!(
        r#"
        update {schema}.cash_discount_terms
        set is_active = false,
            is_default_customer_term = false,
            is_default_supplier_term = false,
            updated_at = now()
        where id = $1
        "#,
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(term_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Skonto-Regel nicht gefunden")?;
    emit_change(&state, "Skonto-Regel deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": term_id })))
}

pub async fn list_quotes(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<QuoteResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "quotes.read").await?;
    let sql = format!(
        r#"
        select id, quote_number, customer_id, status, valid_until::text valid_until,
               cash_discount_term_id, customer_discount_percent::text customer_discount_percent,
               notes_ciphertext, notes_nonce, notes_key_id
        from {schema}.quotes
        order by updated_at desc, quote_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut quotes = Vec::with_capacity(rows.len());
    for row in rows {
        let quote_id = row.get("id");
        quotes.push(quote_from_row(db, &state.crypto, &context.schema_name, row, quote_id).await?);
    }
    Ok(Json(quotes))
}

pub async fn create_quote(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<QuoteRequest>,
) -> Result<Json<QuoteResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "quotes.write").await?;
    let quote_id = Uuid::new_v4();
    let mut payload = payload;
    apply_customer_terms_to_quote(db, &context.schema_name, &mut payload).await?;
    validate_quote_request(&payload)?;
    payload.quote_number =
        next_number_if_blank(db, &context.schema_name, "quotes", &payload.quote_number).await?;
    write_quote(db, &state.crypto, &context, quote_id, &payload, true).await?;
    emit_change(&state, "Angebot angelegt");
    Ok(Json(quote_response(quote_id, payload)))
}

pub async fn update_quote(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(quote_id): Path<Uuid>,
    Json(payload): Json<QuoteRequest>,
) -> Result<Json<QuoteResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "quotes.write").await?;
    let mut payload = payload;
    apply_customer_terms_to_quote(db, &context.schema_name, &mut payload).await?;
    validate_quote_request(&payload)?;
    write_quote(db, &state.crypto, &context, quote_id, &payload, false).await?;
    emit_change(&state, "Angebot geändert");
    Ok(Json(quote_response(quote_id, payload)))
}

pub async fn delete_quote(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(quote_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "quotes.delete").await?;
    let sql = format!(
        "update {schema}.quotes set status='cancelled', updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(quote_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Angebot nicht gefunden")?;
    emit_change(&state, "Angebot storniert");
    Ok(Json(json!({ "deleted": true, "id": quote_id })))
}

pub async fn convert_quote_to_outgoing_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(quote_id): Path<Uuid>,
) -> Result<Json<OutgoingInvoiceResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "quotes.convert_to_invoice").await?;
    let quote = load_quote_by_id(db, &state.crypto, &context.schema_name, quote_id).await?;
    let invoice_id = Uuid::new_v4();
    let invoice = OutgoingInvoiceRequest {
        invoice_number: next_number(db, &context.schema_name, "outgoing_invoices").await?,
        customer_id: quote.customer_id,
        status: "draft".to_string(),
        cash_discount_term_id: quote.cash_discount_term_id,
        customer_discount_percent: quote.customer_discount_percent.clone(),
        issued_at: None,
        due_at: None,
        source_quote_id: Some(quote.id),
        items: quote
            .items
            .iter()
            .map(|item| OutgoingInvoiceItemRequest {
                line_kind: item.line_kind.clone(),
                item_id: item.item_id,
                activity_id: item.activity_id,
                description: item.description.clone(),
                quantity: item.quantity.clone(),
                unit_price: item.unit_price.clone(),
                original_unit_price: item.original_unit_price.clone(),
                discount_percent: item.discount_percent.clone(),
                tax_rate: item.tax_rate.clone(),
            })
            .collect(),
    };
    validate_outgoing_invoice_request(&invoice)?;
    write_outgoing_invoice(db, &state.crypto, &context, invoice_id, &invoice, true).await?;
    emit_change(&state, "Angebot in Rechnung umgewandelt");
    Ok(Json(outgoing_invoice_response(invoice_id, invoice, None)))
}

pub async fn list_outgoing_invoices(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<OutgoingInvoiceResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "outgoing_invoices.read").await?;
    let sql = format!(
        r#"
        select id, invoice_number, customer_id, status, cash_discount_term_id,
               customer_discount_percent::text customer_discount_percent,
               issued_at::text issued_at, due_at::text due_at, source_quote_id, finalized_at
        from {schema}.outgoing_invoices
        order by updated_at desc, invoice_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut invoices = Vec::with_capacity(rows.len());
    for row in rows {
        let invoice_id = row.get("id");
        invoices.push(
            outgoing_invoice_from_row(db, &state.crypto, &context.schema_name, row, invoice_id)
                .await?,
        );
    }
    Ok(Json(invoices))
}

pub async fn create_outgoing_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<OutgoingInvoiceRequest>,
) -> Result<Json<OutgoingInvoiceResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "outgoing_invoices.write").await?;
    let invoice_id = Uuid::new_v4();
    let mut payload = payload;
    apply_customer_terms_to_outgoing_invoice(db, &context.schema_name, &mut payload).await?;
    validate_outgoing_invoice_request(&payload)?;
    payload.invoice_number = next_number_if_blank(
        db,
        &context.schema_name,
        "outgoing_invoices",
        &payload.invoice_number,
    )
    .await?;
    write_outgoing_invoice(db, &state.crypto, &context, invoice_id, &payload, true).await?;
    emit_change(&state, "Ausgangsrechnung angelegt");
    Ok(Json(outgoing_invoice_response(invoice_id, payload, None)))
}

pub async fn update_outgoing_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(invoice_id): Path<Uuid>,
    Json(payload): Json<OutgoingInvoiceRequest>,
) -> Result<Json<OutgoingInvoiceResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "outgoing_invoices.write").await?;
    ensure_outgoing_invoice_editable(db, &context.schema_name, invoice_id).await?;
    let mut payload = payload;
    apply_customer_terms_to_outgoing_invoice(db, &context.schema_name, &mut payload).await?;
    validate_outgoing_invoice_request(&payload)?;
    write_outgoing_invoice(db, &state.crypto, &context, invoice_id, &payload, false).await?;
    emit_change(&state, "Ausgangsrechnung geändert");
    Ok(Json(outgoing_invoice_response(invoice_id, payload, None)))
}

pub async fn delete_outgoing_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(invoice_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "outgoing_invoices.delete").await?;
    ensure_outgoing_invoice_editable(db, &context.schema_name, invoice_id).await?;
    let sql = format!(
        "update {schema}.outgoing_invoices set status='cancelled', updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(invoice_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Ausgangsrechnung nicht gefunden")?;
    emit_change(&state, "Ausgangsrechnung storniert");
    Ok(Json(json!({ "deleted": true, "id": invoice_id })))
}

pub async fn finalize_outgoing_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(invoice_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "invoices.finalize").await?;
    let sql = format!(
        r#"
        update {schema}.outgoing_invoices
        set status='finalized', finalized_at=coalesce(finalized_at, now()), updated_at=now()
        where id=$1 and finalized_at is null
        "#,
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(invoice_id).execute(db).await?;
    ensure_changed(
        result.rows_affected(),
        "Rechnung nicht gefunden oder bereits abgeschlossen",
    )?;
    emit_change(&state, "Ausgangsrechnung abgeschlossen");
    Ok(Json(json!({ "finalized": true, "id": invoice_id })))
}

pub async fn list_incoming_invoices(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<IncomingInvoiceResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "incoming_invoices.read").await?;
    let sql = format!(
        r#"
        select id, invoice_number, supplier_id, status, cash_discount_term_id,
               invoice_date::text invoice_date, due_at::text due_at
        from {schema}.incoming_invoices
        order by updated_at desc, invoice_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut invoices = Vec::with_capacity(rows.len());
    for row in rows {
        let invoice_id = row.get("id");
        invoices.push(
            incoming_invoice_from_row(db, &state.crypto, &context.schema_name, row, invoice_id)
                .await?,
        );
    }
    Ok(Json(invoices))
}

pub async fn create_incoming_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<IncomingInvoiceRequest>,
) -> Result<Json<IncomingInvoiceResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "incoming_invoices.write").await?;
    let invoice_id = Uuid::new_v4();
    let mut payload = payload;
    apply_supplier_terms_to_incoming_invoice(db, &context.schema_name, &mut payload).await?;
    payload.invoice_number = next_number_if_blank(
        db,
        &context.schema_name,
        "incoming_invoices",
        &payload.invoice_number,
    )
    .await?;
    validate_incoming_invoice_request(&payload)?;
    write_incoming_invoice(db, &state.crypto, &context, invoice_id, &payload, true).await?;
    emit_change(&state, "Eingangsrechnung angelegt");
    Ok(Json(incoming_invoice_response(invoice_id, payload)))
}

pub async fn update_incoming_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(invoice_id): Path<Uuid>,
    Json(payload): Json<IncomingInvoiceRequest>,
) -> Result<Json<IncomingInvoiceResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "incoming_invoices.write").await?;
    let mut payload = payload;
    apply_supplier_terms_to_incoming_invoice(db, &context.schema_name, &mut payload).await?;
    validate_incoming_invoice_request(&payload)?;
    write_incoming_invoice(db, &state.crypto, &context, invoice_id, &payload, false).await?;
    emit_change(&state, "Eingangsrechnung geändert");
    Ok(Json(incoming_invoice_response(invoice_id, payload)))
}

pub async fn delete_incoming_invoice(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(invoice_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "incoming_invoices.delete").await?;
    let sql = format!(
        "update {schema}.incoming_invoices set status='cancelled', updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(invoice_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Eingangsrechnung nicht gefunden")?;
    emit_change(&state, "Eingangsrechnung storniert");
    Ok(Json(json!({ "deleted": true, "id": invoice_id })))
}

pub async fn preview_price_list_import(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<PriceListImportRequest>,
) -> Result<Json<PriceListImportPreview>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_lists.import").await?;
    let rows = parse_price_list_rows(db, &context.schema_name, &payload).await?;
    let valid_rows = rows.iter().filter(|row| row.error.is_none()).count();
    let error_rows = rows.len().saturating_sub(valid_rows);
    Ok(Json(PriceListImportPreview {
        total_rows: rows.len(),
        valid_rows,
        error_rows,
        rows,
    }))
}

pub async fn apply_price_list_import(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<PriceListImportRequest>,
) -> Result<Json<PriceListImportApplyResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_lists.import").await?;
    let rows = parse_price_list_rows(db, &context.schema_name, &payload).await?;
    let valid_rows = rows
        .iter()
        .filter(|row| row.error.is_none())
        .cloned()
        .collect::<Vec<_>>();
    let import_id = Uuid::new_v4();
    let insert_import = format!(
        r#"
        insert into {schema}.imports (
            id, import_type, source_name, status, total_rows, applied_rows,
            error_rows, created_by_user_id, finished_at
        ) values ($1, 'price_list', $2, 'applied', $3, $4, $5, $6, now())
        "#,
        schema = context.schema_name
    );
    sqlx::query(&insert_import)
        .bind(import_id)
        .bind(payload.source_name.trim())
        .bind(rows.len() as i32)
        .bind(valid_rows.len() as i32)
        .bind((rows.len() - valid_rows.len()) as i32)
        .bind(context.user_id)
        .execute(db)
        .await?;

    for row in &valid_rows {
        upsert_imported_item(db, &state.crypto, &context, import_id, row).await?;
    }

    emit_change(&state, "Preislistenimport abgeschlossen");
    Ok(Json(PriceListImportApplyResponse {
        import_id,
        applied_rows: valid_rows.len(),
        error_rows: rows.len() - valid_rows.len(),
    }))
}

pub async fn list_api_connectors(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<ApiConnectorResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_apis.sync").await?;
    let sql = format!(
        r#"
        select id, code, name, connector_type, config_ciphertext, config_nonce,
               config_key_id, is_active, sync_interval_minutes, last_sync_at
        from {schema}.api_connectors
        order by name
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut connectors = Vec::with_capacity(rows.len());
    for row in rows {
        connectors.push(api_connector_from_row(&state.crypto, row)?);
    }
    Ok(Json(connectors))
}

pub async fn create_api_connector(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<ApiConnectorRequest>,
) -> Result<Json<ApiConnectorResponse>, ApiError> {
    validate_api_connector_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_apis.sync").await?;
    let connector_id = Uuid::new_v4();
    write_api_connector(
        db,
        &state.crypto,
        &context.schema_name,
        connector_id,
        &payload,
        true,
    )
    .await?;
    emit_change(&state, "Preis-API-Connector angelegt");
    Ok(Json(api_connector_response(connector_id, payload, None)))
}

pub async fn update_api_connector(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(connector_id): Path<Uuid>,
    Json(payload): Json<ApiConnectorRequest>,
) -> Result<Json<ApiConnectorResponse>, ApiError> {
    validate_api_connector_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_apis.sync").await?;
    write_api_connector(
        db,
        &state.crypto,
        &context.schema_name,
        connector_id,
        &payload,
        false,
    )
    .await?;
    emit_change(&state, "Preis-API-Connector geändert");
    Ok(Json(api_connector_response(connector_id, payload, None)))
}

pub async fn delete_api_connector(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(connector_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_apis.sync").await?;
    let sql = format!(
        "update {schema}.api_connectors set is_active=false, updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(connector_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "API-Connector nicht gefunden")?;
    emit_change(&state, "Preis-API-Connector deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": connector_id })))
}

pub async fn sync_api_connector(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(connector_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_apis.sync").await?;
    let select_sql = format!(
        r#"
        select id, code, name, connector_type, config_ciphertext, config_nonce,
               config_key_id, is_active, sync_interval_minutes, last_sync_at
        from {schema}.api_connectors
        where id=$1 and is_active=true
        "#,
        schema = context.schema_name
    );
    let row = sqlx::query(&select_sql)
        .bind(connector_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("API-Connector nicht gefunden oder inaktiv"))?;
    let connector = api_connector_from_row(&state.crypto, row)?;
    let mut applied_rows = 0usize;
    let mut error_rows = 0usize;

    if let Some(content) = connector
        .config
        .get("price_list_csv")
        .and_then(|value| value.as_str())
        .filter(|value| !value.trim().is_empty())
    {
        let payload = PriceListImportRequest {
            source_name: format!("api:{}", connector.code),
            content: content.to_string(),
            delimiter: connector
                .config
                .get("delimiter")
                .and_then(|value| value.as_str())
                .map(ToString::to_string),
        };
        let rows = parse_price_list_rows(db, &context.schema_name, &payload).await?;
        let valid_rows = rows
            .iter()
            .filter(|row| row.error.is_none())
            .cloned()
            .collect::<Vec<_>>();
        let import_id = Uuid::new_v4();
        let insert_import = format!(
            r#"
            insert into {schema}.imports (
                id, import_type, source_name, status, total_rows, applied_rows,
                error_rows, created_by_user_id, finished_at
            ) values ($1, 'api_price_sync', $2, 'applied', $3, $4, $5, $6, now())
            "#,
            schema = context.schema_name
        );
        sqlx::query(&insert_import)
            .bind(import_id)
            .bind(&payload.source_name)
            .bind(rows.len() as i32)
            .bind(valid_rows.len() as i32)
            .bind((rows.len() - valid_rows.len()) as i32)
            .bind(context.user_id)
            .execute(db)
            .await?;

        for row in &valid_rows {
            upsert_imported_item(db, &state.crypto, &context, import_id, row).await?;
        }
        applied_rows = valid_rows.len();
        error_rows = rows.len() - valid_rows.len();
    }

    let update_sql = format!(
        "update {schema}.api_connectors set last_sync_at=now(), updated_at=now() where id=$1 and is_active=true",
        schema = context.schema_name
    );
    let result = sqlx::query(&update_sql)
        .bind(connector_id)
        .execute(db)
        .await?;
    ensure_changed(
        result.rows_affected(),
        "API-Connector nicht gefunden oder inaktiv",
    )?;
    emit_change(&state, "Preis-API-Abgleich ausgeführt");
    Ok(Json(json!({
        "synced": true,
        "id": connector_id,
        "applied_rows": applied_rows,
        "error_rows": error_rows
    })))
}

pub async fn list_price_rules(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<PriceRuleResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_rules.write").await?;
    let sql = format!(
        r#"
        select id, code, name, source_type, source_id, markup_percent::text markup_percent,
               rounding_mode, is_active
        from {schema}.price_rules
        order by name
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    Ok(Json(rows.into_iter().map(price_rule_from_row).collect()))
}

pub async fn create_price_rule(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<PriceRuleRequest>,
) -> Result<Json<PriceRuleResponse>, ApiError> {
    validate_price_rule_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_rules.write").await?;
    let rule_id = Uuid::new_v4();
    write_price_rule(db, &context.schema_name, rule_id, &payload, true).await?;
    emit_change(&state, "Preisregel angelegt");
    Ok(Json(price_rule_response(rule_id, payload)))
}

pub async fn update_price_rule(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(rule_id): Path<Uuid>,
    Json(payload): Json<PriceRuleRequest>,
) -> Result<Json<PriceRuleResponse>, ApiError> {
    validate_price_rule_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_rules.write").await?;
    write_price_rule(db, &context.schema_name, rule_id, &payload, false).await?;
    emit_change(&state, "Preisregel geändert");
    Ok(Json(price_rule_response(rule_id, payload)))
}

pub async fn delete_price_rule(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(rule_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "price_rules.write").await?;
    let sql = format!(
        "update {schema}.price_rules set is_active=false, updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(rule_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Preisregel nicht gefunden")?;
    emit_change(&state, "Preisregel deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": rule_id })))
}

pub async fn list_communications(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<CommunicationResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "communication.read").await?;
    let sql = format!(
        r#"
        select id, communication_type, direction, subject_ciphertext, subject_nonce,
               subject_key_id, body_ciphertext, body_nonce, body_key_id, status, occurred_at
        from {schema}.communications
        where status <> 'archived'
        order by coalesce(occurred_at, updated_at) desc
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut records = Vec::with_capacity(rows.len());
    for row in rows {
        records.push(communication_from_row(db, &state.crypto, &context.schema_name, row).await?);
    }
    Ok(Json(records))
}

pub async fn create_communication(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<CommunicationRequest>,
) -> Result<Json<CommunicationResponse>, ApiError> {
    validate_communication_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "communication.write").await?;
    let communication_id = Uuid::new_v4();
    write_communication(
        db,
        &state.crypto,
        &context,
        communication_id,
        &payload,
        true,
    )
    .await?;
    emit_change(&state, "Kommunikation gespeichert");
    get_communication_by_id(db, &state.crypto, &context.schema_name, communication_id).await
}

pub async fn update_communication(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(communication_id): Path<Uuid>,
    Json(payload): Json<CommunicationRequest>,
) -> Result<Json<CommunicationResponse>, ApiError> {
    validate_communication_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "communication.write").await?;
    write_communication(
        db,
        &state.crypto,
        &context,
        communication_id,
        &payload,
        false,
    )
    .await?;
    emit_change(&state, "Kommunikation geändert");
    get_communication_by_id(db, &state.crypto, &context.schema_name, communication_id).await
}

pub async fn delete_communication(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(communication_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "communication.write").await?;
    let sql = format!(
        "update {schema}.communications set status='archived', updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(communication_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Kommunikation nicht gefunden")?;
    emit_change(&state, "Kommunikation archiviert");
    Ok(Json(json!({ "deleted": true, "id": communication_id })))
}

pub async fn list_documents(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<DocumentResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "documents.read").await?;
    let sql = format!(
        r#"
        select id, title_ciphertext, title_nonce, title_key_id,
               description_ciphertext, description_nonce, description_key_id, status
        from {schema}.documents
        where status <> 'deleted'
        order by updated_at desc
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut documents = Vec::with_capacity(rows.len());
    for row in rows {
        documents.push(document_from_row(db, &state.crypto, &context.schema_name, row).await?);
    }
    Ok(Json(documents))
}

pub async fn upload_document(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<DocumentUploadRequest>,
) -> Result<Json<DocumentResponse>, ApiError> {
    validate_document_upload_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "documents.write").await?;
    let content = BASE64_STANDARD
        .decode(payload.content_base64.trim())
        .map_err(|_| ApiError::bad_request("Dateiinhalt ist kein gültiges Base64"))?;
    if content.len() > 20 * 1024 * 1024 {
        return Err(ApiError::bad_request("Datei ist größer als 20 MB"));
    }
    let document_id = Uuid::new_v4();
    let version_id = Uuid::new_v4();
    let encrypted_content = state.crypto.encrypt(&content)?;
    let checksum = Sha256::digest(&content)
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    let storage_path = document_storage_path(&context.schema_name, document_id, version_id)?;
    if let Some(parent) = storage_path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }
    tokio::fs::write(
        &storage_path,
        serde_json::to_vec(&json!({
            "ciphertext": BASE64_STANDARD.encode(encrypted_content.ciphertext),
            "nonce": BASE64_STANDARD.encode(encrypted_content.nonce),
            "key_id": encrypted_content.key_id
        }))?,
    )
    .await?;

    let title = state.crypto.encrypt(&payload.title.trim().to_string())?;
    let description = encrypted_optional_string(&state.crypto, &payload.description)?;
    let file_name = state
        .crypto
        .encrypt(&payload.file_name.trim().to_string())?;
    let content_type = state
        .crypto
        .encrypt(&payload.content_type.trim().to_string())?;
    let insert_document = format!(
        r#"
        insert into {schema}.documents (
            id, title_ciphertext, title_nonce, title_key_id,
            description_ciphertext, description_nonce, description_key_id, created_by_user_id
        ) values ($1,$2,$3,$4,$5,$6,$7,$8)
        "#,
        schema = context.schema_name
    );
    sqlx::query(&insert_document)
        .bind(document_id)
        .bind(title.ciphertext)
        .bind(title.nonce)
        .bind(title.key_id)
        .bind(description.as_ref().map(|value| value.ciphertext.clone()))
        .bind(description.as_ref().map(|value| value.nonce.clone()))
        .bind(description.as_ref().map(|value| value.key_id.clone()))
        .bind(context.user_id)
        .execute(db)
        .await?;

    let insert_version = format!(
        r#"
        insert into {schema}.document_versions (
            id, document_id, version_no, file_name_ciphertext, file_name_nonce,
            file_name_key_id, content_type_ciphertext, content_type_nonce,
            content_type_key_id, file_size, storage_path, checksum_sha256, uploaded_by_user_id
        ) values ($1,$2,1,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
        "#,
        schema = context.schema_name
    );
    sqlx::query(&insert_version)
        .bind(version_id)
        .bind(document_id)
        .bind(file_name.ciphertext)
        .bind(file_name.nonce)
        .bind(file_name.key_id)
        .bind(content_type.ciphertext)
        .bind(content_type.nonce)
        .bind(content_type.key_id)
        .bind(content.len() as i64)
        .bind(storage_path.to_string_lossy().to_string())
        .bind(checksum)
        .bind(context.user_id)
        .execute(db)
        .await?;
    write_document_links(db, &context.schema_name, document_id, &payload.links).await?;
    insert_document_audit(
        db,
        &context.schema_name,
        document_id,
        Some(version_id),
        "upload",
        context.user_id,
    )
    .await?;
    emit_change(&state, "Dokument hochgeladen");
    get_document_by_id(db, &state.crypto, &context.schema_name, document_id).await
}

pub async fn download_document(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(document_id): Path<Uuid>,
) -> Result<Json<DocumentDownloadResponse>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "documents.read").await?;
    let sql = format!(
        r#"
        select id, document_id, version_no, file_name_ciphertext, file_name_nonce,
               file_name_key_id, content_type_ciphertext, content_type_nonce,
               content_type_key_id, file_size, storage_path, checksum_sha256, created_at
        from {schema}.document_versions
        where document_id=$1
        order by version_no desc
        limit 1
        "#,
        schema = context.schema_name
    );
    let row = sqlx::query(&sql)
        .bind(document_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Dokument nicht gefunden"))?;
    let version_id: Uuid = row.get("id");
    let file_name = state.crypto.decrypt(
        &row.get::<Vec<u8>, _>("file_name_ciphertext"),
        &row.get::<Vec<u8>, _>("file_name_nonce"),
        &row.get::<String, _>("file_name_key_id"),
    )?;
    let content_type = state.crypto.decrypt(
        &row.get::<Vec<u8>, _>("content_type_ciphertext"),
        &row.get::<Vec<u8>, _>("content_type_nonce"),
        &row.get::<String, _>("content_type_key_id"),
    )?;
    let storage_path = row.get::<String, _>("storage_path");
    let envelope = tokio::fs::read(&storage_path).await?;
    let envelope = serde_json::from_slice::<serde_json::Value>(&envelope)?;
    let ciphertext = envelope
        .get("ciphertext")
        .and_then(|value| value.as_str())
        .ok_or_else(|| ApiError::internal("Dokumentenspeicher ist beschädigt".to_string()))
        .and_then(|value| {
            BASE64_STANDARD
                .decode(value)
                .map_err(|_| ApiError::internal("Dokumentenspeicher ist beschädigt".to_string()))
        })?;
    let nonce = envelope
        .get("nonce")
        .and_then(|value| value.as_str())
        .ok_or_else(|| ApiError::internal("Dokumentenspeicher ist beschädigt".to_string()))
        .and_then(|value| {
            BASE64_STANDARD
                .decode(value)
                .map_err(|_| ApiError::internal("Dokumentenspeicher ist beschädigt".to_string()))
        })?;
    let key_id = envelope
        .get("key_id")
        .and_then(|value| value.as_str())
        .ok_or_else(|| ApiError::internal("Dokumentenspeicher ist beschädigt".to_string()))?;
    let content: Vec<u8> = state.crypto.decrypt(&ciphertext, &nonce, key_id)?;
    insert_document_audit(
        db,
        &context.schema_name,
        document_id,
        Some(version_id),
        "download",
        context.user_id,
    )
    .await?;
    Ok(Json(DocumentDownloadResponse {
        document_id,
        version_id,
        file_name,
        content_type,
        content_base64: BASE64_STANDARD.encode(content),
    }))
}

pub async fn delete_document(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(document_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "documents.write").await?;
    let sql = format!(
        "update {schema}.documents set status='deleted', updated_at=now() where id=$1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(document_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Dokument nicht gefunden")?;
    insert_document_audit(
        db,
        &context.schema_name,
        document_id,
        None,
        "archive",
        context.user_id,
    )
    .await?;
    emit_change(&state, "Dokument archiviert");
    Ok(Json(json!({ "deleted": true, "id": document_id })))
}

pub async fn list_document_audit_log(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(document_id): Path<Uuid>,
) -> Result<Json<Vec<DocumentAuditLogResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "documents.read").await?;
    let sql = format!(
        r#"
        select id, document_id, version_id, action, user_id, created_at
        from {schema}.document_audit_log
        where document_id=$1
        order by created_at desc
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).bind(document_id).fetch_all(db).await?;
    Ok(Json(
        rows.into_iter()
            .map(|row| DocumentAuditLogResponse {
                id: row.get("id"),
                document_id: row.get("document_id"),
                version_id: row.get("version_id"),
                action: row.get("action"),
                user_id: row.get("user_id"),
                created_at: row.get("created_at"),
            })
            .collect(),
    ))
}

pub async fn list_items(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<ItemResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "items.read").await?;
    let sql = format!(
        r#"
        select id, item_number, manufacturer_code, name_ciphertext, name_nonce, name_key_id, unit,
               tax_rate::text tax_rate, default_purchase_price::text default_purchase_price,
               default_sales_price::text default_sales_price, status
        from {schema}.items order by updated_at desc, item_number
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut items = Vec::with_capacity(rows.len());
    for row in rows {
        items.push(item_from_row(db, &state.crypto, &context.schema_name, row).await?);
    }
    Ok(Json(items))
}

pub async fn create_item(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<ItemRequest>,
) -> Result<Json<ItemResponse>, ApiError> {
    validate_item_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "items.write").await?;
    let item_id = Uuid::new_v4();
    let mut payload = payload;
    payload.item_number =
        next_number_if_blank(db, &context.schema_name, "items", &payload.item_number).await?;
    write_item(
        db,
        &state.crypto,
        &context.schema_name,
        item_id,
        &payload,
        true,
    )
    .await?;
    save_item_supplier_prices(db, &context.schema_name, item_id, &payload, "manual").await?;
    record_item_price_history(db, &context.schema_name, item_id, &payload, context.user_id).await?;
    emit_change(&state, "Artikel angelegt");
    Ok(Json(
        load_item_response_by_id(db, &state.crypto, &context.schema_name, item_id).await?,
    ))
}

pub async fn update_item(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(item_id): Path<Uuid>,
    Json(payload): Json<ItemRequest>,
) -> Result<Json<ItemResponse>, ApiError> {
    validate_item_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "items.write").await?;
    write_item(
        db,
        &state.crypto,
        &context.schema_name,
        item_id,
        &payload,
        false,
    )
    .await?;
    save_item_supplier_prices(db, &context.schema_name, item_id, &payload, "manual").await?;
    record_item_price_history(db, &context.schema_name, item_id, &payload, context.user_id).await?;
    emit_change(&state, "Artikel geändert");
    Ok(Json(
        load_item_response_by_id(db, &state.crypto, &context.schema_name, item_id).await?,
    ))
}

pub async fn list_item_price_history(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(item_id): Path<Uuid>,
) -> Result<Json<Vec<ItemPriceHistoryResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "items.read").await?;
    let sql = format!(
        r#"
        select id, item_id, purchase_price::text purchase_price,
               sales_price::text sales_price, source, valid_from,
               created_by_user_id, created_at
        from {schema}.item_price_history
        where item_id = $1
        order by valid_from desc, created_at desc
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).bind(item_id).fetch_all(db).await?;
    Ok(Json(
        rows.into_iter().map(item_price_history_from_row).collect(),
    ))
}

pub async fn delete_item(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(item_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "items.delete").await?;
    set_record_inactive(db, &context.schema_name, "items", item_id).await?;
    emit_change(&state, "Artikel deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": item_id })))
}

pub async fn list_activities(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<ActivityResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "activities.read").await?;
    let sql = format!(
        r#"
        select id, activity_number, activity_type, title_ciphertext, title_nonce, title_key_id,
               body_ciphertext, body_nonce, body_key_id, status, priority, due_at
        from {schema}.activities order by updated_at desc, due_at
        "#,
        schema = context.schema_name
    );
    let rows = sqlx::query(&sql).fetch_all(db).await?;
    let mut activities = Vec::with_capacity(rows.len());
    for row in rows {
        activities.push(activity_from_row(&state.crypto, row)?);
    }
    Ok(Json(activities))
}

pub async fn create_activity(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<ActivityRequest>,
) -> Result<Json<ActivityResponse>, ApiError> {
    validate_activity_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "activities.write").await?;
    let activity_id = Uuid::new_v4();
    let mut payload = payload;
    if payload
        .activity_number
        .as_deref()
        .unwrap_or("")
        .trim()
        .is_empty()
    {
        payload.activity_number = Some(next_number(db, &context.schema_name, "activities").await?);
    }
    write_activity(db, &state.crypto, &context, activity_id, &payload, true).await?;
    emit_change(&state, "Aktivität angelegt");
    Ok(Json(activity_response(activity_id, payload)))
}

pub async fn update_activity(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(activity_id): Path<Uuid>,
    Json(payload): Json<ActivityRequest>,
) -> Result<Json<ActivityResponse>, ApiError> {
    validate_activity_request(&payload)?;
    let db = state.db()?;
    let context = require_permission(db, &headers, "activities.write").await?;
    write_activity(db, &state.crypto, &context, activity_id, &payload, false).await?;
    emit_change(&state, "Aktivität geändert");
    Ok(Json(activity_response(activity_id, payload)))
}

pub async fn delete_activity(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(activity_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "activities.delete").await?;
    let sql = format!(
        "update {schema}.activities set status = 'inactive', updated_at = now() where id = $1",
        schema = context.schema_name
    );
    let result = sqlx::query(&sql).bind(activity_id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Aktivität nicht gefunden")?;
    emit_change(&state, "Aktivität deaktiviert");
    Ok(Json(json!({ "deleted": true, "id": activity_id })))
}

pub async fn list_users(
    headers: HeaderMap,
    State(state): State<AppState>,
) -> Result<Json<Vec<OrganizationUserResponse>>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "users.read").await?;
    let organization_id = context.organization_id;
    let schema_name = context.schema_name;
    let rows = sqlx::query(
        r#"
        select u.id, u.email, uo.status
        from users u
        join user_organizations uo on uo.user_id = u.id
        where uo.organization_id = $1
        order by u.email
        "#,
    )
    .bind(organization_id)
    .fetch_all(db)
    .await?;

    let mut response = Vec::with_capacity(rows.len());
    for row in rows {
        let user_id: Uuid = row.get("id");
        let roles_sql = format!(
            "select r.code from {schema}.roles r join {schema}.user_roles ur on ur.role_id = r.id where ur.user_id = $1 order by r.code",
            schema = schema_name
        );
        let roles = sqlx::query_scalar::<_, String>(&roles_sql)
            .bind(user_id)
            .fetch_all(db)
            .await?;

        response.push(OrganizationUserResponse {
            user_id,
            email: row.get("email"),
            status: row.get("status"),
            roles,
        });
    }

    Ok(Json(response))
}

pub async fn invite_user(
    headers: HeaderMap,
    State(state): State<AppState>,
    Json(payload): Json<InviteUserRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "users.invite").await?;
    require_owner(db, &context.schema_name, context.user_id).await?;
    let organization_id = context.organization_id;
    let actor_user_id = context.user_id;
    let schema_name = context.schema_name;
    let email = normalize_email(&payload.email)?;
    let user_id = ensure_user(db, &email).await?;
    let invitation_token = generate_token();
    let invitation_token_hash = hash_token(&invitation_token);

    sqlx::query(
        r#"
        update users
        set must_change_password = true,
            initial_password_expires_at = now() + interval '7 days',
            updated_at = now()
        where id = $1 and password_hash is null
        "#,
    )
    .bind(user_id)
    .execute(db)
    .await?;

    sqlx::query(
        r#"
        insert into user_organizations (user_id, organization_id, status, invited_by_user_id, invited_at)
        values ($1, $2, 'pending_invitation', $3, now())
        on conflict (user_id, organization_id) do update set
            invited_by_user_id = excluded.invited_by_user_id,
            invited_at = now(),
            updated_at = now()
        "#,
    )
    .bind(user_id)
    .bind(organization_id)
    .bind(actor_user_id)
    .execute(db)
    .await?;

    let invitation_id = Uuid::new_v4();
    sqlx::query(
        r#"
        insert into user_invitations (
            id, organization_id, email, invited_by_user_id, expires_at, created_user_id, token_hash
        ) values ($1, $2, $3, $4, now() + interval '7 days', $5, $6)
        "#,
    )
    .bind(invitation_id)
    .bind(organization_id)
    .bind(&email)
    .bind(actor_user_id)
    .bind(user_id)
    .bind(invitation_token_hash)
    .execute(db)
    .await?;

    let roles = if payload.roles.is_empty() {
        vec!["viewer".to_string()]
    } else {
        payload.roles
    };
    for role in roles {
        assign_role(db, &schema_name, user_id, &role).await?;
    }
    enqueue_invitation_email(
        &state.crypto,
        db,
        &email,
        &invitation_token,
        organization_id,
    )
    .await?;

    let mut response = json!({
        "id": invitation_id,
        "user_id": user_id
    });
    if dev_mode_enabled() {
        response["dev_invitation_token"] = json!(invitation_token);
    }
    Ok(Json(response))
}

pub async fn update_user_roles(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(user_id): Path<Uuid>,
    Json(payload): Json<UpdateUserRolesRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "users.roles.write").await?;
    require_owner(db, &context.schema_name, context.user_id).await?;
    let organization_id = context.organization_id;
    let actor_user_id = context.user_id;
    let schema_name = context.schema_name;

    if payload.roles.is_empty() {
        return Err(ApiError::bad_request(
            "Mindestens eine Rolle ist erforderlich",
        ));
    }

    let member_exists: Option<Uuid> = sqlx::query_scalar(
        "select user_id from user_organizations where organization_id = $1 and user_id = $2 limit 1",
    )
    .bind(organization_id)
    .bind(user_id)
    .fetch_optional(db)
    .await?;

    if member_exists.is_none() {
        return Err(ApiError::not_found("Benutzer ist nicht Teil dieser Firma"));
    }

    if user_id == actor_user_id && !payload.roles.iter().any(|role| role == "owner") {
        return Err(ApiError::bad_request(
            "Der Besitzer kann sich die Besitzerrolle nicht selbst entziehen",
        ));
    }

    replace_user_roles(db, &schema_name, user_id, &payload.roles).await?;
    emit_change(&state, "Benutzerrechte geändert");

    Ok(Json(json!({
        "saved": true,
        "user_id": user_id,
        "roles": payload.roles
    })))
}

pub async fn disable_user(
    headers: HeaderMap,
    State(state): State<AppState>,
    Path(user_id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let db = state.db()?;
    let context = require_permission(db, &headers, "users.disable").await?;
    require_owner(db, &context.schema_name, context.user_id).await?;

    if user_id == context.user_id {
        return Err(ApiError::bad_request(
            "Der Besitzer kann sich nicht selbst deaktivieren",
        ));
    }

    let result = sqlx::query(
        r#"
        update user_organizations
        set status = 'disabled', updated_at = now()
        where organization_id = $1 and user_id = $2
        "#,
    )
    .bind(context.organization_id)
    .bind(user_id)
    .execute(db)
    .await?;

    if result.rows_affected() == 0 {
        return Err(ApiError::not_found("Benutzer ist nicht Teil dieser Firma"));
    }

    emit_change(&state, "Benutzer deaktiviert");
    Ok(Json(json!({ "disabled": true, "user_id": user_id })))
}

fn validate_customer_request(payload: &CustomerRequest) -> Result<(), ApiError> {
    if payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request("Kundenname ist zu kurz"));
    }
    if !["active", "inactive", "blocked"].contains(&payload.status.as_str()) {
        return Err(ApiError::bad_request("Ungültiger Kundenstatus"));
    }
    let discount = payload
        .standard_discount_percent
        .parse::<f64>()
        .map_err(|_| ApiError::bad_request("Ungültiger Kundenrabatt"))?;
    if !(0.0..=100.0).contains(&discount) {
        return Err(ApiError::bad_request(
            "Kundenrabatt muss zwischen 0 und 100 liegen",
        ));
    }
    Ok(())
}

fn customer_response(customer_id: Uuid, payload: CustomerRequest) -> CustomerResponse {
    CustomerResponse {
        id: customer_id,
        customer_number: payload.customer_number.trim().to_string(),
        name: payload.name.trim().to_string(),
        status: payload.status,
        details: payload.details,
        standard_discount_percent: payload.standard_discount_percent,
        cash_discount_term_id: payload.cash_discount_term_id,
    }
}

fn customer_from_row(
    crypto: &DataCrypto,
    row: sqlx::postgres::PgRow,
) -> Result<CustomerResponse, ApiError> {
    let details = match row.get::<Option<Vec<u8>>, _>("details_ciphertext") {
        Some(ciphertext) => crypto.decrypt::<CustomerDetails>(
            &ciphertext,
            &row.get::<Vec<u8>, _>("details_nonce"),
            &row.get::<String, _>("details_key_id"),
        )?,
        None => CustomerDetails::default(),
    };
    Ok(CustomerResponse {
        id: row.get("id"),
        customer_number: row
            .get::<Option<String>, _>("customer_number")
            .unwrap_or_default(),
        name: crypto.decrypt(
            &row.get::<Vec<u8>, _>("name_ciphertext"),
            &row.get::<Vec<u8>, _>("name_nonce"),
            &row.get::<String, _>("name_key_id"),
        )?,
        status: row.get("status"),
        details,
        standard_discount_percent: row.get("standard_discount_percent"),
        cash_discount_term_id: row.get("cash_discount_term_id"),
    })
}

async fn save_customer_terms(
    db: &PgPool,
    schema_name: &str,
    customer_id: Uuid,
    payload: &CustomerRequest,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let deactivate_sql = format!(
        "update {schema}.customer_price_terms set is_active = false, updated_at = now() where customer_id = $1 and is_active = true",
        schema = schema_name
    );
    sqlx::query(&deactivate_sql)
        .bind(customer_id)
        .execute(db)
        .await?;
    let insert_sql = format!(
        r#"
        insert into {schema}.customer_price_terms (
            id, customer_id, standard_discount_percent, cash_discount_term_id
        ) values ($1, $2, $3::numeric, $4)
        "#,
        schema = schema_name
    );
    sqlx::query(&insert_sql)
        .bind(Uuid::new_v4())
        .bind(customer_id)
        .bind(payload.standard_discount_percent.trim())
        .bind(payload.cash_discount_term_id)
        .execute(db)
        .await?;
    Ok(())
}

fn validate_supplier_request(payload: &SupplierRequest) -> Result<(), ApiError> {
    if payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request("Lieferantenname ist erforderlich"));
    }
    validate_status(&payload.status)?;
    validate_percent(&payload.standard_discount_percent, "Lieferantenrabatt")?;
    if payload.payment_days.is_some_and(|days| days < 0) {
        return Err(ApiError::bad_request(
            "Zahlungsziel darf nicht negativ sein",
        ));
    }
    Ok(())
}

async fn write_supplier(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    supplier_id: Uuid,
    payload: &SupplierRequest,
    insert: bool,
) -> Result<(), ApiError> {
    let name = crypto.encrypt(&payload.name.trim().to_string())?;
    let details = crypto.encrypt(&payload.details)?;
    let sql = if insert {
        format!(
            r#"insert into {schema}.suppliers (
                id, supplier_number, name_ciphertext, name_nonce, name_key_id, status,
                details_ciphertext, details_nonce, details_key_id
            ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9)"#,
            schema = schema_name
        )
    } else {
        format!(
            r#"update {schema}.suppliers set supplier_number=supplier_number, name_ciphertext=$3,
                name_nonce=$4, name_key_id=$5, status=$6, details_ciphertext=$7,
                details_nonce=$8, details_key_id=$9, updated_at=now() where id=$1"#,
            schema = schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(supplier_id)
        .bind(payload.supplier_number.trim())
        .bind(name.ciphertext)
        .bind(name.nonce)
        .bind(name.key_id)
        .bind(&payload.status)
        .bind(details.ciphertext)
        .bind(details.nonce)
        .bind(details.key_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Lieferant nicht gefunden")?;
    }
    Ok(())
}

async fn save_supplier_terms(
    db: &PgPool,
    schema_name: &str,
    supplier_id: Uuid,
    payload: &SupplierRequest,
) -> Result<(), ApiError> {
    let deactivate = format!(
        "update {schema}.supplier_price_terms set is_active=false, updated_at=now() where supplier_id=$1 and is_active=true",
        schema = schema_name
    );
    sqlx::query(&deactivate)
        .bind(supplier_id)
        .execute(db)
        .await?;
    let insert = format!(
        r#"insert into {schema}.supplier_price_terms (
            id, supplier_id, standard_discount_percent, cash_discount_term_id, payment_days
        ) values ($1,$2,$3::numeric,$4,$5)"#,
        schema = schema_name
    );
    sqlx::query(&insert)
        .bind(Uuid::new_v4())
        .bind(supplier_id)
        .bind(payload.standard_discount_percent.trim())
        .bind(payload.cash_discount_term_id)
        .bind(payload.payment_days)
        .execute(db)
        .await?;
    Ok(())
}

async fn apply_customer_terms_to_quote(
    db: &PgPool,
    schema_name: &str,
    payload: &mut QuoteRequest,
) -> Result<(), ApiError> {
    if let Some((discount, cash_discount_term_id)) =
        load_customer_terms(db, schema_name, payload.customer_id).await?
    {
        if payload.customer_discount_percent.trim().is_empty()
            || payload.customer_discount_percent.trim() == "0"
            || payload.customer_discount_percent.trim() == "0.00"
        {
            payload.customer_discount_percent = discount;
        }
        if payload.cash_discount_term_id.is_none() {
            payload.cash_discount_term_id = cash_discount_term_id;
        }
    }
    Ok(())
}

async fn apply_customer_terms_to_outgoing_invoice(
    db: &PgPool,
    schema_name: &str,
    payload: &mut OutgoingInvoiceRequest,
) -> Result<(), ApiError> {
    if let Some((discount, cash_discount_term_id)) =
        load_customer_terms(db, schema_name, payload.customer_id).await?
    {
        if payload.customer_discount_percent.trim().is_empty()
            || payload.customer_discount_percent.trim() == "0"
            || payload.customer_discount_percent.trim() == "0.00"
        {
            payload.customer_discount_percent = discount;
        }
        if payload.cash_discount_term_id.is_none() {
            payload.cash_discount_term_id = cash_discount_term_id;
        }
    }
    Ok(())
}

async fn apply_supplier_terms_to_incoming_invoice(
    db: &PgPool,
    schema_name: &str,
    payload: &mut IncomingInvoiceRequest,
) -> Result<(), ApiError> {
    if payload.cash_discount_term_id.is_none() {
        payload.cash_discount_term_id =
            load_supplier_cash_discount_term(db, schema_name, payload.supplier_id).await?;
    }
    Ok(())
}

async fn load_customer_terms(
    db: &PgPool,
    schema_name: &str,
    customer_id: Uuid,
) -> Result<Option<(String, Option<Uuid>)>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select standard_discount_percent::text standard_discount_percent,
               cash_discount_term_id
        from {schema}.customer_price_terms
        where customer_id=$1 and is_active=true
        order by updated_at desc
        limit 1
        "#,
        schema = schema_name
    );
    Ok(sqlx::query(&sql)
        .bind(customer_id)
        .fetch_optional(db)
        .await?
        .map(|row| {
            (
                row.get("standard_discount_percent"),
                row.get("cash_discount_term_id"),
            )
        }))
}

async fn load_supplier_cash_discount_term(
    db: &PgPool,
    schema_name: &str,
    supplier_id: Uuid,
) -> Result<Option<Uuid>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select cash_discount_term_id
        from {schema}.supplier_price_terms
        where supplier_id=$1 and is_active=true
        order by updated_at desc
        limit 1
        "#,
        schema = schema_name
    );
    Ok(sqlx::query_scalar::<_, Option<Uuid>>(&sql)
        .bind(supplier_id)
        .fetch_optional(db)
        .await?
        .flatten())
}

fn supplier_from_row(
    crypto: &DataCrypto,
    row: sqlx::postgres::PgRow,
) -> Result<SupplierResponse, ApiError> {
    let details = match row.get::<Option<Vec<u8>>, _>("details_ciphertext") {
        Some(ciphertext) => crypto.decrypt::<CustomerDetails>(
            &ciphertext,
            &row.get::<Vec<u8>, _>("details_nonce"),
            &row.get::<String, _>("details_key_id"),
        )?,
        None => CustomerDetails::default(),
    };
    Ok(SupplierResponse {
        id: row.get("id"),
        supplier_number: row
            .get::<Option<String>, _>("supplier_number")
            .unwrap_or_default(),
        name: crypto.decrypt(
            &row.get::<Vec<u8>, _>("name_ciphertext"),
            &row.get::<Vec<u8>, _>("name_nonce"),
            &row.get::<String, _>("name_key_id"),
        )?,
        status: row.get("status"),
        details,
        standard_discount_percent: row.get("standard_discount_percent"),
        cash_discount_term_id: row.get("cash_discount_term_id"),
        payment_days: row.get("payment_days"),
    })
}

fn supplier_response(id: Uuid, payload: SupplierRequest) -> SupplierResponse {
    SupplierResponse {
        id,
        supplier_number: payload.supplier_number.trim().to_string(),
        name: payload.name.trim().to_string(),
        status: payload.status,
        details: payload.details,
        standard_discount_percent: payload.standard_discount_percent,
        cash_discount_term_id: payload.cash_discount_term_id,
        payment_days: payload.payment_days,
    }
}

fn validate_cash_discount_term(payload: &CashDiscountTermRequest) -> Result<(), ApiError> {
    if payload.code.trim().is_empty() || payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request(
            "Code und Name der Skonto-Regel sind erforderlich",
        ));
    }
    validate_percent(&payload.discount_percent, "Skonto")?;
    if payload.discount_days < 0 {
        return Err(ApiError::bad_request(
            "Skontotage dürfen nicht negativ sein",
        ));
    }
    if let Some(net_days) = payload.net_days {
        if net_days < payload.discount_days {
            return Err(ApiError::bad_request(
                "Netto-Zahlungsziel darf nicht kleiner als die Skontofrist sein",
            ));
        }
    }
    Ok(())
}

async fn write_cash_discount_term(
    db: &PgPool,
    schema_name: &str,
    term_id: Uuid,
    payload: &CashDiscountTermRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    if payload.is_default_customer_term {
        let sql = format!(
            "update {schema}.cash_discount_terms set is_default_customer_term=false, updated_at=now() where id <> $1",
            schema = schema_name
        );
        sqlx::query(&sql).bind(term_id).execute(db).await?;
    }
    if payload.is_default_supplier_term {
        let sql = format!(
            "update {schema}.cash_discount_terms set is_default_supplier_term=false, updated_at=now() where id <> $1",
            schema = schema_name
        );
        sqlx::query(&sql).bind(term_id).execute(db).await?;
    }

    let sql = if insert {
        format!(
            r#"
            insert into {schema}.cash_discount_terms (
                id, code, name, discount_percent, discount_days, net_days,
                valid_from, valid_until, is_default_customer_term,
                is_default_supplier_term, is_active
            ) values ($1,$2,$3,$4::numeric,$5,$6,$7::date,$8::date,$9,$10,$11)
            "#,
            schema = schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.cash_discount_terms
            set code=$2, name=$3, discount_percent=$4::numeric,
                discount_days=$5, net_days=$6, valid_from=$7::date,
                valid_until=$8::date, is_default_customer_term=$9,
                is_default_supplier_term=$10, is_active=$11, updated_at=now()
            where id=$1
            "#,
            schema = schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(term_id)
        .bind(payload.code.trim())
        .bind(payload.name.trim())
        .bind(payload.discount_percent.trim())
        .bind(payload.discount_days)
        .bind(payload.net_days)
        .bind(
            payload
                .valid_from
                .as_deref()
                .filter(|value| !value.is_empty()),
        )
        .bind(
            payload
                .valid_until
                .as_deref()
                .filter(|value| !value.is_empty()),
        )
        .bind(payload.is_default_customer_term)
        .bind(payload.is_default_supplier_term)
        .bind(payload.is_active)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Skonto-Regel nicht gefunden")?;
    }
    Ok(())
}

fn cash_discount_term_response(
    id: Uuid,
    payload: CashDiscountTermRequest,
) -> CashDiscountTermResponse {
    CashDiscountTermResponse {
        id,
        code: payload.code.trim().to_string(),
        name: payload.name.trim().to_string(),
        discount_percent: payload.discount_percent.trim().to_string(),
        discount_days: payload.discount_days,
        net_days: payload.net_days,
        valid_from: payload.valid_from,
        valid_until: payload.valid_until,
        is_default_customer_term: payload.is_default_customer_term,
        is_default_supplier_term: payload.is_default_supplier_term,
        is_active: payload.is_active,
    }
}

fn cash_discount_term_from_row(row: sqlx::postgres::PgRow) -> CashDiscountTermResponse {
    CashDiscountTermResponse {
        id: row.get("id"),
        code: row.get("code"),
        name: row.get("name"),
        discount_percent: row.get("discount_percent"),
        discount_days: row.get("discount_days"),
        net_days: row.get("net_days"),
        valid_from: row.get("valid_from"),
        valid_until: row.get("valid_until"),
        is_default_customer_term: row.get("is_default_customer_term"),
        is_default_supplier_term: row.get("is_default_supplier_term"),
        is_active: row.get("is_active"),
    }
}

fn validate_quote_request(payload: &QuoteRequest) -> Result<(), ApiError> {
    validate_status_choice(
        &payload.status,
        &[
            "draft",
            "sent",
            "accepted",
            "rejected",
            "expired",
            "cancelled",
        ],
        "Ungültiger Angebotsstatus",
    )?;
    validate_percent(&payload.customer_discount_percent, "Kundenrabatt")?;
    if payload.items.is_empty() {
        return Err(ApiError::bad_request(
            "Ein Angebot braucht mindestens eine Position",
        ));
    }
    for item in &payload.items {
        validate_line_source(&item.line_kind, item.item_id, item.activity_id)?;
        validate_number(&item.quantity, "Menge")?;
        if item.quantity.parse::<f64>().unwrap_or(0.0) <= 0.0 {
            return Err(ApiError::bad_request("Menge muss größer als 0 sein"));
        }
        validate_number(&item.unit_price, "Positionspreis")?;
        validate_tax_rate(&item.tax_rate)?;
        validate_percent(&item.discount_percent, "Positionsrabatt")?;
        if let Some(original_price) = &item.original_unit_price {
            validate_number(original_price, "Originalpreis")?;
        }
    }
    Ok(())
}

async fn write_quote(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    quote_id: Uuid,
    payload: &QuoteRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(&context.schema_name)?;
    ensure_customer_exists(db, &context.schema_name, payload.customer_id).await?;
    for item in &payload.items {
        if let Some(item_id) = item.item_id {
            ensure_item_exists(db, &context.schema_name, item_id).await?;
        }
        if let Some(activity_id) = item.activity_id {
            ensure_activity_exists(db, &context.schema_name, activity_id).await?;
        }
    }
    let notes = if payload.notes.trim().is_empty() {
        None
    } else {
        Some(crypto.encrypt(&payload.notes)?)
    };
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.quotes (
                id, quote_number, customer_id, status, valid_until,
                cash_discount_term_id, customer_discount_percent,
                notes_ciphertext, notes_nonce, notes_key_id, created_by_user_id
            ) values ($1,$2,$3,$4,$5::date,$6,$7::numeric,$8,$9,$10,$11)
            "#,
            schema = context.schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.quotes
            set quote_number=quote_number, customer_id=$3, status=$4, valid_until=$5::date,
                cash_discount_term_id=$6, customer_discount_percent=$7::numeric,
                notes_ciphertext=$8, notes_nonce=$9, notes_key_id=$10,
                updated_at=now()
            where id=$1
            "#,
            schema = context.schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(quote_id)
        .bind(payload.quote_number.trim())
        .bind(payload.customer_id)
        .bind(&payload.status)
        .bind(
            payload
                .valid_until
                .as_deref()
                .filter(|value| !value.is_empty()),
        )
        .bind(payload.cash_discount_term_id)
        .bind(payload.customer_discount_percent.trim())
        .bind(notes.as_ref().map(|field| field.ciphertext.clone()))
        .bind(notes.as_ref().map(|field| field.nonce.clone()))
        .bind(notes.as_ref().map(|field| field.key_id.clone()))
        .bind(context.user_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Angebot nicht gefunden")?;
        let delete_sql = format!(
            "delete from {schema}.quote_items where quote_id=$1",
            schema = context.schema_name
        );
        sqlx::query(&delete_sql).bind(quote_id).execute(db).await?;
    }
    write_quote_items(db, crypto, &context.schema_name, quote_id, &payload.items).await
}

async fn write_quote_items(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    quote_id: Uuid,
    items: &[QuoteItemRequest],
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.quote_items (
            id, quote_id, line_number, line_kind, item_id, activity_id, description_ciphertext,
            description_nonce, description_key_id, quantity, unit_price,
            original_unit_price, discount_percent, price_overridden, tax_rate
        ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::numeric,$11::numeric,$12::numeric,$13::numeric,$14,$15::numeric)
        "#,
        schema = schema_name
    );
    for (index, item) in items.iter().enumerate() {
        let description = if item.description.trim().is_empty() {
            None
        } else {
            Some(crypto.encrypt(&item.description)?)
        };
        let price_overridden = item
            .original_unit_price
            .as_ref()
            .is_some_and(|original| original.trim() != item.unit_price.trim());
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(quote_id)
            .bind((index + 1) as i32)
            .bind(item.line_kind.trim())
            .bind(item.item_id)
            .bind(item.activity_id)
            .bind(description.as_ref().map(|field| field.ciphertext.clone()))
            .bind(description.as_ref().map(|field| field.nonce.clone()))
            .bind(description.as_ref().map(|field| field.key_id.clone()))
            .bind(item.quantity.trim())
            .bind(item.unit_price.trim())
            .bind(item.original_unit_price.as_deref())
            .bind(item.discount_percent.trim())
            .bind(price_overridden)
            .bind(item.tax_rate.trim())
            .execute(db)
            .await?;
    }
    Ok(())
}

async fn ensure_customer_exists(
    db: &PgPool,
    schema_name: &str,
    customer_id: Uuid,
) -> Result<(), ApiError> {
    let sql = format!(
        "select exists(select 1 from {schema}.customers where id=$1 and status='active')",
        schema = schema_name
    );
    if sqlx::query_scalar::<_, bool>(&sql)
        .bind(customer_id)
        .fetch_one(db)
        .await?
    {
        Ok(())
    } else {
        Err(ApiError::bad_request(
            "Kunde ist nicht vorhanden oder inaktiv",
        ))
    }
}

async fn ensure_item_exists(db: &PgPool, schema_name: &str, item_id: Uuid) -> Result<(), ApiError> {
    let sql = format!(
        "select exists(select 1 from {schema}.items where id=$1 and status='active')",
        schema = schema_name
    );
    if sqlx::query_scalar::<_, bool>(&sql)
        .bind(item_id)
        .fetch_one(db)
        .await?
    {
        Ok(())
    } else {
        Err(ApiError::bad_request(
            "Artikel ist nicht vorhanden oder inaktiv",
        ))
    }
}

async fn ensure_activity_exists(
    db: &PgPool,
    schema_name: &str,
    activity_id: Uuid,
) -> Result<(), ApiError> {
    let sql = format!(
        "select exists(select 1 from {schema}.activities where id=$1 and status = 'active')",
        schema = schema_name
    );
    if sqlx::query_scalar::<_, bool>(&sql)
        .bind(activity_id)
        .fetch_one(db)
        .await?
    {
        Ok(())
    } else {
        Err(ApiError::bad_request(
            "Aktivität ist nicht vorhanden oder inaktiv",
        ))
    }
}

fn validate_line_source(
    line_kind: &str,
    item_id: Option<Uuid>,
    activity_id: Option<Uuid>,
) -> Result<(), ApiError> {
    match line_kind {
        "item" if item_id.is_some() && activity_id.is_none() => Ok(()),
        "activity" if activity_id.is_some() && item_id.is_none() => Ok(()),
        "item" => Err(ApiError::bad_request("Artikelposition braucht einen Artikel")),
        "activity" => Err(ApiError::bad_request(
            "Aktivitätsposition braucht eine Aktivität",
        )),
        _ => Err(ApiError::bad_request("Ungültiger Positionstyp")),
    }
}

fn quote_response(id: Uuid, payload: QuoteRequest) -> QuoteResponse {
    QuoteResponse {
        id,
        quote_number: payload.quote_number.trim().to_string(),
        customer_id: payload.customer_id,
        status: payload.status,
        valid_until: payload.valid_until,
        cash_discount_term_id: payload.cash_discount_term_id,
        customer_discount_percent: payload.customer_discount_percent,
        notes: payload.notes,
        items: payload
            .items
            .into_iter()
            .enumerate()
            .map(|(index, item)| {
                let price_overridden = item
                    .original_unit_price
                    .as_ref()
                    .is_some_and(|original| original.trim() != item.unit_price.trim());
                QuoteItemResponse {
                    id: Uuid::new_v4(),
                    line_number: (index + 1) as i32,
                    line_kind: item.line_kind,
                    item_id: item.item_id,
                    activity_id: item.activity_id,
                    description: item.description,
                    quantity: item.quantity,
                    unit_price: item.unit_price,
                    original_unit_price: item.original_unit_price,
                    discount_percent: item.discount_percent,
                    tax_rate: item.tax_rate,
                    price_overridden,
                }
            })
            .collect(),
    }
}

async fn quote_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
    quote_id: Uuid,
) -> Result<QuoteResponse, ApiError> {
    let notes = match row.get::<Option<Vec<u8>>, _>("notes_ciphertext") {
        Some(ciphertext) => crypto.decrypt::<String>(
            &ciphertext,
            &row.get::<Vec<u8>, _>("notes_nonce"),
            &row.get::<String, _>("notes_key_id"),
        )?,
        None => String::new(),
    };
    Ok(QuoteResponse {
        id: quote_id,
        quote_number: row.get("quote_number"),
        customer_id: row.get("customer_id"),
        status: row.get("status"),
        valid_until: row.get("valid_until"),
        cash_discount_term_id: row.get("cash_discount_term_id"),
        customer_discount_percent: row.get("customer_discount_percent"),
        notes,
        items: load_quote_items(db, crypto, schema_name, quote_id).await?,
    })
}

async fn load_quote_items(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    quote_id: Uuid,
) -> Result<Vec<QuoteItemResponse>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select id, line_number, line_kind, item_id, activity_id, description_ciphertext, description_nonce,
               description_key_id, quantity::text quantity, unit_price::text unit_price,
               original_unit_price::text original_unit_price,
               discount_percent::text discount_percent, tax_rate::text tax_rate,
               price_overridden
        from {schema}.quote_items
        where quote_id=$1
        order by line_number
        "#,
        schema = schema_name
    );
    let rows = sqlx::query(&sql).bind(quote_id).fetch_all(db).await?;
    let mut items = Vec::with_capacity(rows.len());
    for row in rows {
        let description = match row.get::<Option<Vec<u8>>, _>("description_ciphertext") {
            Some(ciphertext) => crypto.decrypt::<String>(
                &ciphertext,
                &row.get::<Vec<u8>, _>("description_nonce"),
                &row.get::<String, _>("description_key_id"),
            )?,
            None => String::new(),
        };
        items.push(QuoteItemResponse {
            id: row.get("id"),
            line_number: row.get("line_number"),
            line_kind: row.get("line_kind"),
            item_id: row.get("item_id"),
            activity_id: row.get("activity_id"),
            description,
            quantity: row.get("quantity"),
            unit_price: row.get("unit_price"),
            original_unit_price: row.get("original_unit_price"),
            discount_percent: row.get("discount_percent"),
            tax_rate: row.get("tax_rate"),
            price_overridden: row.get("price_overridden"),
        });
    }
    Ok(items)
}

async fn load_quote_by_id(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    quote_id: Uuid,
) -> Result<QuoteResponse, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select id, quote_number, customer_id, status, valid_until::text valid_until,
               cash_discount_term_id, customer_discount_percent::text customer_discount_percent,
               notes_ciphertext, notes_nonce, notes_key_id
        from {schema}.quotes
        where id=$1
        "#,
        schema = schema_name
    );
    let row = sqlx::query(&sql)
        .bind(quote_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Angebot nicht gefunden"))?;
    quote_from_row(db, crypto, schema_name, row, quote_id).await
}

fn validate_outgoing_invoice_request(payload: &OutgoingInvoiceRequest) -> Result<(), ApiError> {
    validate_status_choice(
        &payload.status,
        &["draft", "finalized", "sent", "paid", "cancelled", "overdue"],
        "Ungültiger Rechnungsstatus",
    )?;
    validate_percent(&payload.customer_discount_percent, "Kundenrabatt")?;
    if payload.items.is_empty() {
        return Err(ApiError::bad_request(
            "Eine Rechnung braucht mindestens eine Position",
        ));
    }
    for item in &payload.items {
        validate_line_source(&item.line_kind, item.item_id, item.activity_id)?;
        validate_number(&item.quantity, "Menge")?;
        if item.quantity.parse::<f64>().unwrap_or(0.0) <= 0.0 {
            return Err(ApiError::bad_request("Menge muss größer als 0 sein"));
        }
        validate_number(&item.unit_price, "Positionspreis")?;
        validate_tax_rate(&item.tax_rate)?;
        validate_percent(&item.discount_percent, "Positionsrabatt")?;
        if let Some(original_price) = &item.original_unit_price {
            validate_number(original_price, "Originalpreis")?;
        }
    }
    Ok(())
}

async fn write_outgoing_invoice(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    invoice_id: Uuid,
    payload: &OutgoingInvoiceRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(&context.schema_name)?;
    ensure_customer_exists(db, &context.schema_name, payload.customer_id).await?;
    for item in &payload.items {
        if let Some(item_id) = item.item_id {
            ensure_item_exists(db, &context.schema_name, item_id).await?;
        }
        if let Some(activity_id) = item.activity_id {
            ensure_activity_exists(db, &context.schema_name, activity_id).await?;
        }
    }
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.outgoing_invoices (
                id, invoice_number, customer_id, status, cash_discount_term_id,
                customer_discount_percent, issued_at, due_at, source_quote_id, created_by_user_id
            ) values ($1,$2,$3,$4,$5,$6::numeric,$7::date,$8::date,$9,$10)
            "#,
            schema = context.schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.outgoing_invoices
            set invoice_number=invoice_number, customer_id=$3, status=$4, cash_discount_term_id=$5,
                customer_discount_percent=$6::numeric, issued_at=$7::date,
                due_at=$8::date, source_quote_id=$9, updated_at=now()
            where id=$1
            "#,
            schema = context.schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(invoice_id)
        .bind(payload.invoice_number.trim())
        .bind(payload.customer_id)
        .bind(&payload.status)
        .bind(payload.cash_discount_term_id)
        .bind(payload.customer_discount_percent.trim())
        .bind(
            payload
                .issued_at
                .as_deref()
                .filter(|value| !value.is_empty()),
        )
        .bind(payload.due_at.as_deref().filter(|value| !value.is_empty()))
        .bind(payload.source_quote_id)
        .bind(context.user_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Ausgangsrechnung nicht gefunden")?;
        let delete_sql = format!(
            "delete from {schema}.outgoing_invoice_items where invoice_id=$1",
            schema = context.schema_name
        );
        sqlx::query(&delete_sql)
            .bind(invoice_id)
            .execute(db)
            .await?;
    }
    write_outgoing_invoice_items(db, crypto, context, invoice_id, &payload.items).await
}

async fn write_outgoing_invoice_items(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    invoice_id: Uuid,
    items: &[OutgoingInvoiceItemRequest],
) -> Result<(), ApiError> {
    let sql = format!(
        r#"
        insert into {schema}.outgoing_invoice_items (
            id, invoice_id, line_number, line_kind, item_id, activity_id, description_ciphertext,
            description_nonce, description_key_id, quantity, unit_price,
            original_unit_price, discount_percent, price_overridden,
            price_overridden_by_user_id, price_overridden_at, tax_rate
        ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::numeric,$11::numeric,$12::numeric,$13::numeric,$14,$15,$16,$17::numeric)
        "#,
        schema = context.schema_name
    );
    for (index, item) in items.iter().enumerate() {
        let description = if item.description.trim().is_empty() {
            None
        } else {
            Some(crypto.encrypt(&item.description)?)
        };
        let price_overridden = item
            .original_unit_price
            .as_ref()
            .is_some_and(|original| original.trim() != item.unit_price.trim());
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(invoice_id)
            .bind((index + 1) as i32)
            .bind(item.line_kind.trim())
            .bind(item.item_id)
            .bind(item.activity_id)
            .bind(description.as_ref().map(|field| field.ciphertext.clone()))
            .bind(description.as_ref().map(|field| field.nonce.clone()))
            .bind(description.as_ref().map(|field| field.key_id.clone()))
            .bind(item.quantity.trim())
            .bind(item.unit_price.trim())
            .bind(item.original_unit_price.as_deref())
            .bind(item.discount_percent.trim())
            .bind(price_overridden)
            .bind(if price_overridden {
                Some(context.user_id)
            } else {
                None
            })
            .bind(if price_overridden {
                Some(Utc::now())
            } else {
                None
            })
            .bind(item.tax_rate.trim())
            .execute(db)
            .await?;
    }
    Ok(())
}

fn outgoing_invoice_response(
    id: Uuid,
    payload: OutgoingInvoiceRequest,
    finalized_at: Option<DateTime<Utc>>,
) -> OutgoingInvoiceResponse {
    OutgoingInvoiceResponse {
        id,
        invoice_number: payload.invoice_number.trim().to_string(),
        customer_id: payload.customer_id,
        status: payload.status,
        cash_discount_term_id: payload.cash_discount_term_id,
        customer_discount_percent: payload.customer_discount_percent,
        issued_at: payload.issued_at,
        due_at: payload.due_at,
        source_quote_id: payload.source_quote_id,
        finalized_at,
        items: payload
            .items
            .into_iter()
            .enumerate()
            .map(|(index, item)| {
                let price_overridden = item
                    .original_unit_price
                    .as_ref()
                    .is_some_and(|original| original.trim() != item.unit_price.trim());
                OutgoingInvoiceItemResponse {
                    id: Uuid::new_v4(),
                    line_number: (index + 1) as i32,
                    line_kind: item.line_kind,
                    item_id: item.item_id,
                    activity_id: item.activity_id,
                    description: item.description,
                    quantity: item.quantity,
                    unit_price: item.unit_price,
                    original_unit_price: item.original_unit_price,
                    discount_percent: item.discount_percent,
                    tax_rate: item.tax_rate,
                    price_overridden,
                }
            })
            .collect(),
    }
}

async fn outgoing_invoice_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
    invoice_id: Uuid,
) -> Result<OutgoingInvoiceResponse, ApiError> {
    Ok(OutgoingInvoiceResponse {
        id: invoice_id,
        invoice_number: row
            .get::<Option<String>, _>("invoice_number")
            .unwrap_or_default(),
        customer_id: row.get("customer_id"),
        status: row.get("status"),
        cash_discount_term_id: row.get("cash_discount_term_id"),
        customer_discount_percent: row.get("customer_discount_percent"),
        issued_at: row.get("issued_at"),
        due_at: row.get("due_at"),
        source_quote_id: row.get("source_quote_id"),
        finalized_at: row.get("finalized_at"),
        items: load_outgoing_invoice_items(db, crypto, schema_name, invoice_id).await?,
    })
}

async fn load_outgoing_invoice_items(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    invoice_id: Uuid,
) -> Result<Vec<OutgoingInvoiceItemResponse>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select id, line_number, line_kind, item_id, activity_id, description_ciphertext, description_nonce,
               description_key_id, quantity::text quantity, unit_price::text unit_price,
               original_unit_price::text original_unit_price,
               discount_percent::text discount_percent, tax_rate::text tax_rate,
               price_overridden
        from {schema}.outgoing_invoice_items
        where invoice_id=$1
        order by line_number
        "#,
        schema = schema_name
    );
    let rows = sqlx::query(&sql).bind(invoice_id).fetch_all(db).await?;
    let mut items = Vec::with_capacity(rows.len());
    for row in rows {
        let description = match row.get::<Option<Vec<u8>>, _>("description_ciphertext") {
            Some(ciphertext) => crypto.decrypt::<String>(
                &ciphertext,
                &row.get::<Vec<u8>, _>("description_nonce"),
                &row.get::<String, _>("description_key_id"),
            )?,
            None => String::new(),
        };
        items.push(OutgoingInvoiceItemResponse {
            id: row.get("id"),
            line_number: row.get("line_number"),
            line_kind: row.get("line_kind"),
            item_id: row.get("item_id"),
            activity_id: row.get("activity_id"),
            description,
            quantity: row.get("quantity"),
            unit_price: row.get("unit_price"),
            original_unit_price: row.get("original_unit_price"),
            discount_percent: row.get("discount_percent"),
            tax_rate: row.get("tax_rate"),
            price_overridden: row.get("price_overridden"),
        });
    }
    Ok(items)
}

async fn ensure_outgoing_invoice_editable(
    db: &PgPool,
    schema_name: &str,
    invoice_id: Uuid,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        "select finalized_at from {schema}.outgoing_invoices where id=$1",
        schema = schema_name
    );
    let finalized_at = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(&sql)
        .bind(invoice_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Ausgangsrechnung nicht gefunden"))?;
    if finalized_at.is_some() {
        Err(ApiError::bad_request(
            "Abgeschlossene Rechnungen dürfen nicht geändert werden",
        ))
    } else {
        Ok(())
    }
}

fn validate_incoming_invoice_request(payload: &IncomingInvoiceRequest) -> Result<(), ApiError> {
    validate_status_choice(
        &payload.status,
        &[
            "draft",
            "received",
            "approved",
            "paid",
            "cancelled",
            "overdue",
        ],
        "Ungültiger Eingangsrechnungsstatus",
    )?;
    if payload.invoice_number.trim().is_empty() {
        return Err(ApiError::bad_request(
            "Rechnungsnummer des Lieferanten fehlt",
        ));
    }
    if payload.items.is_empty() {
        return Err(ApiError::bad_request(
            "Eine Eingangsrechnung braucht mindestens eine Position",
        ));
    }
    for item in &payload.items {
        validate_number(&item.quantity, "Menge")?;
        if item.quantity.parse::<f64>().unwrap_or(0.0) <= 0.0 {
            return Err(ApiError::bad_request("Menge muss größer als 0 sein"));
        }
        validate_number(&item.unit_price, "Positionspreis")?;
        validate_tax_rate(&item.tax_rate)?;
    }
    Ok(())
}

async fn write_incoming_invoice(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    invoice_id: Uuid,
    payload: &IncomingInvoiceRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_supplier_exists(db, &context.schema_name, payload.supplier_id).await?;
    for item in &payload.items {
        if let Some(item_id) = item.item_id {
            ensure_item_exists(db, &context.schema_name, item_id).await?;
        }
    }
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.incoming_invoices (
                id, invoice_number, supplier_id, status, cash_discount_term_id,
                invoice_date, due_at, created_by_user_id
            ) values ($1,$2,$3,$4,$5,$6::date,$7::date,$8)
            "#,
            schema = context.schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.incoming_invoices
            set invoice_number=invoice_number, supplier_id=$3, status=$4,
                cash_discount_term_id=$5, invoice_date=$6::date,
                due_at=$7::date, updated_at=now()
            where id=$1
            "#,
            schema = context.schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(invoice_id)
        .bind(payload.invoice_number.trim())
        .bind(payload.supplier_id)
        .bind(&payload.status)
        .bind(payload.cash_discount_term_id)
        .bind(
            payload
                .invoice_date
                .as_deref()
                .filter(|value| !value.is_empty()),
        )
        .bind(payload.due_at.as_deref().filter(|value| !value.is_empty()))
        .bind(context.user_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Eingangsrechnung nicht gefunden")?;
        let delete_sql = format!(
            "delete from {schema}.incoming_invoice_items where invoice_id=$1",
            schema = context.schema_name
        );
        sqlx::query(&delete_sql)
            .bind(invoice_id)
            .execute(db)
            .await?;
    }
    write_incoming_invoice_items(db, crypto, &context.schema_name, invoice_id, &payload.items).await
}

async fn write_incoming_invoice_items(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    invoice_id: Uuid,
    items: &[IncomingInvoiceItemRequest],
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.incoming_invoice_items (
            id, invoice_id, line_number, item_id, description_ciphertext,
            description_nonce, description_key_id, quantity, unit_price, tax_rate
        ) values ($1,$2,$3,$4,$5,$6,$7,$8::numeric,$9::numeric,$10::numeric)
        "#,
        schema = schema_name
    );
    for (index, item) in items.iter().enumerate() {
        let description = if item.description.trim().is_empty() {
            None
        } else {
            Some(crypto.encrypt(&item.description)?)
        };
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(invoice_id)
            .bind((index + 1) as i32)
            .bind(item.item_id)
            .bind(description.as_ref().map(|field| field.ciphertext.clone()))
            .bind(description.as_ref().map(|field| field.nonce.clone()))
            .bind(description.as_ref().map(|field| field.key_id.clone()))
            .bind(item.quantity.trim())
            .bind(item.unit_price.trim())
            .bind(item.tax_rate.trim())
            .execute(db)
            .await?;
    }
    Ok(())
}

fn incoming_invoice_response(id: Uuid, payload: IncomingInvoiceRequest) -> IncomingInvoiceResponse {
    IncomingInvoiceResponse {
        id,
        invoice_number: payload.invoice_number.trim().to_string(),
        supplier_id: payload.supplier_id,
        status: payload.status,
        cash_discount_term_id: payload.cash_discount_term_id,
        invoice_date: payload.invoice_date,
        due_at: payload.due_at,
        items: payload
            .items
            .into_iter()
            .enumerate()
            .map(|(index, item)| IncomingInvoiceItemResponse {
                id: Uuid::new_v4(),
                line_number: (index + 1) as i32,
                item_id: item.item_id,
                description: item.description,
                quantity: item.quantity,
                unit_price: item.unit_price,
                tax_rate: item.tax_rate,
            })
            .collect(),
    }
}

async fn incoming_invoice_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
    invoice_id: Uuid,
) -> Result<IncomingInvoiceResponse, ApiError> {
    Ok(IncomingInvoiceResponse {
        id: invoice_id,
        invoice_number: row
            .get::<Option<String>, _>("invoice_number")
            .unwrap_or_default(),
        supplier_id: row.get("supplier_id"),
        status: row.get("status"),
        cash_discount_term_id: row.get("cash_discount_term_id"),
        invoice_date: row.get("invoice_date"),
        due_at: row.get("due_at"),
        items: load_incoming_invoice_items(db, crypto, schema_name, invoice_id).await?,
    })
}

async fn load_incoming_invoice_items(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    invoice_id: Uuid,
) -> Result<Vec<IncomingInvoiceItemResponse>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select id, line_number, item_id, description_ciphertext, description_nonce,
               description_key_id, quantity::text quantity, unit_price::text unit_price,
               tax_rate::text tax_rate
        from {schema}.incoming_invoice_items
        where invoice_id=$1
        order by line_number
        "#,
        schema = schema_name
    );
    let rows = sqlx::query(&sql).bind(invoice_id).fetch_all(db).await?;
    let mut items = Vec::with_capacity(rows.len());
    for row in rows {
        let description = match row.get::<Option<Vec<u8>>, _>("description_ciphertext") {
            Some(ciphertext) => crypto.decrypt::<String>(
                &ciphertext,
                &row.get::<Vec<u8>, _>("description_nonce"),
                &row.get::<String, _>("description_key_id"),
            )?,
            None => String::new(),
        };
        items.push(IncomingInvoiceItemResponse {
            id: row.get("id"),
            line_number: row.get("line_number"),
            item_id: row.get("item_id"),
            description,
            quantity: row.get("quantity"),
            unit_price: row.get("unit_price"),
            tax_rate: row.get("tax_rate"),
        });
    }
    Ok(items)
}

async fn ensure_supplier_exists(
    db: &PgPool,
    schema_name: &str,
    supplier_id: Uuid,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        "select exists(select 1 from {schema}.suppliers where id=$1 and status='active')",
        schema = schema_name
    );
    if sqlx::query_scalar::<_, bool>(&sql)
        .bind(supplier_id)
        .fetch_one(db)
        .await?
    {
        Ok(())
    } else {
        Err(ApiError::bad_request(
            "Lieferant ist nicht vorhanden oder inaktiv",
        ))
    }
}

async fn parse_price_list_rows(
    db: &PgPool,
    schema_name: &str,
    payload: &PriceListImportRequest,
) -> Result<Vec<PriceListImportRow>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let delimiter = payload
        .delimiter
        .as_deref()
        .filter(|value| !value.is_empty())
        .unwrap_or(";");
    let mut lines = payload
        .content
        .lines()
        .filter(|line| !line.trim().is_empty());
    let header = lines
        .next()
        .ok_or_else(|| ApiError::bad_request("CSV-Datei enthält keine Kopfzeile"))?;
    let headers = split_csv_line(header, delimiter);
    let required = ["item_number", "name", "unit", "tax_rate"];
    for column in required {
        if !headers.iter().any(|header| header == column) {
            return Err(ApiError::bad_request(&format!(
                "CSV-Spalte fehlt: {column}"
            )));
        }
    }
    let mut rows = Vec::new();
    for (index, line) in lines.enumerate() {
        let values = split_csv_line(line, delimiter);
        let value = |name: &str| -> String {
            headers
                .iter()
                .position(|header| header == name)
                .and_then(|position| values.get(position))
                .cloned()
                .unwrap_or_default()
        };
        let row_number = index + 2;
        let item_number = value("item_number");
        let name = value("name");
        let manufacturer_code = empty_string_to_none(value("manufacturer_code"));
        let unit = value("unit");
        let tax_rate = value("tax_rate");
        let purchase_price = empty_string_to_none(value("purchase_price"));
        let sales_price = empty_string_to_none(value("sales_price"));
        let supplier_number = empty_string_to_none(value("supplier_number"));
        let supplier_item_number = empty_string_to_none(value("supplier_item_number"));
        let currency = empty_string_to_none(value("currency")).map(|value| value.to_uppercase());
        let mut error = None;
        if item_number.trim().is_empty() {
            error = Some("Artikelnummer fehlt".to_string());
        } else if name.trim().is_empty() {
            error = Some("Bezeichnung fehlt".to_string());
        } else if validate_tax_rate(&tax_rate).is_err() {
            error = Some("Steuersatz ist ungültig".to_string());
        } else if purchase_price
            .as_ref()
            .is_some_and(|price| parse_number(price).is_err())
        {
            error = Some("Einkaufspreis ist ungültig".to_string());
        } else if sales_price
            .as_ref()
            .is_some_and(|price| parse_number(price).is_err())
        {
            error = Some("Verkaufspreis ist ungültig".to_string());
        } else if currency
            .as_ref()
            .is_some_and(|currency| currency.len() != 3)
        {
            error = Some("Währung muss aus drei Zeichen bestehen".to_string());
        } else if supplier_number.is_some() && supplier_item_number.is_none() {
            error = Some("Externe Lieferanten-Artikelnummer fehlt".to_string());
        } else if supplier_item_number.is_some() && supplier_number.is_none() {
            error = Some("Lieferantennummer fehlt".to_string());
        } else if let Some(supplier_number) = &supplier_number {
            if !supplier_number_exists(db, schema_name, supplier_number).await? {
                error = Some("Lieferant nicht gefunden".to_string());
            }
        }
        let action = if item_number_exists(db, schema_name, &item_number).await? {
            "update"
        } else {
            "create"
        };
        rows.push(PriceListImportRow {
            row_number,
            item_number,
            name,
            manufacturer_code,
            unit: if unit.trim().is_empty() {
                "Stck".to_string()
            } else {
                unit
            },
            tax_rate,
            purchase_price,
            sales_price,
            supplier_number,
            supplier_item_number,
            currency,
            action: action.to_string(),
            error,
        });
    }
    Ok(rows)
}

fn split_csv_line(line: &str, delimiter: &str) -> Vec<String> {
    line.split(delimiter)
        .map(|value| value.trim().trim_matches('"').to_string())
        .collect()
}

fn empty_string_to_none(value: String) -> Option<String> {
    if value.trim().is_empty() {
        None
    } else {
        Some(value)
    }
}

async fn item_number_exists(
    db: &PgPool,
    schema_name: &str,
    item_number: &str,
) -> Result<bool, ApiError> {
    let sql = format!(
        "select exists(select 1 from {schema}.items where item_number=$1)",
        schema = schema_name
    );
    Ok(sqlx::query_scalar::<_, bool>(&sql)
        .bind(item_number.trim())
        .fetch_one(db)
        .await?)
}

async fn supplier_number_exists(
    db: &PgPool,
    schema_name: &str,
    supplier_number: &str,
) -> Result<bool, ApiError> {
    let sql = format!(
        "select exists(select 1 from {schema}.suppliers where supplier_number=$1)",
        schema = schema_name
    );
    Ok(sqlx::query_scalar::<_, bool>(&sql)
        .bind(supplier_number.trim())
        .fetch_one(db)
        .await?)
}

async fn upsert_imported_item(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    import_id: Uuid,
    row: &PriceListImportRow,
) -> Result<(), ApiError> {
    let item_id = match find_item_id_by_number(db, &context.schema_name, &row.item_number).await? {
        Some(item_id) => {
            let name = crypto.encrypt(&row.name)?;
            let sql = format!(
                r#"
                update {schema}.items
                set name_ciphertext=$2, name_nonce=$3, name_key_id=$4,
                    manufacturer_code=$5, unit=$6, tax_rate=$7::numeric,
                    default_purchase_price=$8::numeric,
                    default_sales_price=$9::numeric,
                    status='active', updated_at=now()
                where id=$1
                "#,
                schema = context.schema_name
            );
            sqlx::query(&sql)
                .bind(item_id)
                .bind(name.ciphertext)
                .bind(name.nonce)
                .bind(name.key_id)
                .bind(row.manufacturer_code.as_deref().map(str::trim))
                .bind(row.unit.trim())
                .bind(normalize_number_string(&row.tax_rate))
                .bind(normalize_optional_number(row.purchase_price.as_deref()))
                .bind(normalize_optional_number(row.sales_price.as_deref()))
                .execute(db)
                .await?;
            item_id
        }
        None => {
            let item_id = Uuid::new_v4();
            let name = crypto.encrypt(&row.name)?;
            let sql = format!(
                r#"
                insert into {schema}.items (
                    id, item_number, manufacturer_code, name_ciphertext, name_nonce, name_key_id,
                    unit, tax_rate, default_purchase_price, default_sales_price, status
                ) values ($1,$2,$3,$4,$5,$6,$7,$8::numeric,$9::numeric,$10::numeric,'active')
                "#,
                schema = context.schema_name
            );
            sqlx::query(&sql)
                .bind(item_id)
                .bind(row.item_number.trim())
                .bind(row.manufacturer_code.as_deref().map(str::trim))
                .bind(name.ciphertext)
                .bind(name.nonce)
                .bind(name.key_id)
                .bind(row.unit.trim())
                .bind(normalize_number_string(&row.tax_rate))
                .bind(normalize_optional_number(row.purchase_price.as_deref()))
                .bind(normalize_optional_number(row.sales_price.as_deref()))
                .execute(db)
                .await?;
            item_id
        }
    };
    if let (Some(supplier_number), Some(external_number), Some(purchase_price)) = (
        row.supplier_number.as_deref(),
        row.supplier_item_number.as_deref(),
        row.purchase_price.as_deref(),
    ) {
        let supplier_id = find_supplier_id_by_number(db, &context.schema_name, supplier_number)
            .await?
            .ok_or_else(|| ApiError::bad_request("Lieferant nicht gefunden"))?;
        upsert_item_supplier_price(
            db,
            &context.schema_name,
            item_id,
            supplier_id,
            external_number,
            purchase_price,
            row.currency.as_deref().unwrap_or("EUR"),
            &format!("import:{import_id}"),
        )
        .await?;
    }
    record_item_price_history_with_source(
        db,
        &context.schema_name,
        item_id,
        row.purchase_price.as_deref(),
        row.sales_price.as_deref(),
        &format!("import:{import_id}"),
        context.user_id,
    )
    .await
}

async fn find_item_id_by_number(
    db: &PgPool,
    schema_name: &str,
    item_number: &str,
) -> Result<Option<Uuid>, ApiError> {
    let sql = format!(
        "select id from {schema}.items where item_number=$1",
        schema = schema_name
    );
    Ok(sqlx::query_scalar::<_, Uuid>(&sql)
        .bind(item_number.trim())
        .fetch_optional(db)
        .await?)
}

async fn find_supplier_id_by_number(
    db: &PgPool,
    schema_name: &str,
    supplier_number: &str,
) -> Result<Option<Uuid>, ApiError> {
    let sql = format!(
        "select id from {schema}.suppliers where supplier_number=$1",
        schema = schema_name
    );
    Ok(sqlx::query_scalar::<_, Uuid>(&sql)
        .bind(supplier_number.trim())
        .fetch_optional(db)
        .await?)
}

async fn upsert_item_supplier_price(
    db: &PgPool,
    schema_name: &str,
    item_id: Uuid,
    supplier_id: Uuid,
    external_item_number: &str,
    purchase_price: &str,
    currency: &str,
    source: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.item_supplier_prices (
            id, item_id, supplier_id, external_item_number, purchase_price, currency, source
        ) values ($1,$2,$3,$4,$5::numeric,$6,$7)
        on conflict (supplier_id, external_item_number)
        do update set item_id=excluded.item_id,
                      purchase_price=excluded.purchase_price,
                      currency=excluded.currency,
                      source=excluded.source,
                      updated_at=now()
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(Uuid::new_v4())
        .bind(item_id)
        .bind(supplier_id)
        .bind(external_item_number.trim())
        .bind(normalize_number_string(purchase_price))
        .bind(currency.trim().to_uppercase())
        .bind(source)
        .execute(db)
        .await?;
    Ok(())
}

fn validate_api_connector_request(payload: &ApiConnectorRequest) -> Result<(), ApiError> {
    if payload.code.trim().is_empty() || payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request("Code und Name sind erforderlich"));
    }
    if payload.connector_type.trim().is_empty() {
        return Err(ApiError::bad_request("Connector-Typ fehlt"));
    }
    if payload
        .sync_interval_minutes
        .is_some_and(|minutes| minutes <= 0)
    {
        return Err(ApiError::bad_request(
            "Synchronisationsintervall ist ungültig",
        ));
    }
    Ok(())
}

async fn write_api_connector(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    connector_id: Uuid,
    payload: &ApiConnectorRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let config = crypto.encrypt(&payload.config)?;
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.api_connectors (
                id, code, name, connector_type, config_ciphertext, config_nonce,
                config_key_id, is_active, sync_interval_minutes
            ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9)
            "#,
            schema = schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.api_connectors
            set code=$2, name=$3, connector_type=$4, config_ciphertext=$5,
                config_nonce=$6, config_key_id=$7, is_active=$8,
                sync_interval_minutes=$9, updated_at=now()
            where id=$1
            "#,
            schema = schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(connector_id)
        .bind(payload.code.trim())
        .bind(payload.name.trim())
        .bind(payload.connector_type.trim())
        .bind(config.ciphertext)
        .bind(config.nonce)
        .bind(config.key_id)
        .bind(payload.is_active)
        .bind(payload.sync_interval_minutes)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "API-Connector nicht gefunden")?;
    }
    Ok(())
}

fn api_connector_response(
    id: Uuid,
    payload: ApiConnectorRequest,
    last_sync_at: Option<DateTime<Utc>>,
) -> ApiConnectorResponse {
    ApiConnectorResponse {
        id,
        code: payload.code,
        name: payload.name,
        connector_type: payload.connector_type,
        config: payload.config,
        is_active: payload.is_active,
        sync_interval_minutes: payload.sync_interval_minutes,
        last_sync_at,
    }
}

fn api_connector_from_row(
    crypto: &DataCrypto,
    row: sqlx::postgres::PgRow,
) -> Result<ApiConnectorResponse, ApiError> {
    Ok(ApiConnectorResponse {
        id: row.get("id"),
        code: row.get("code"),
        name: row.get("name"),
        connector_type: row.get("connector_type"),
        config: crypto.decrypt(
            &row.get::<Vec<u8>, _>("config_ciphertext"),
            &row.get::<Vec<u8>, _>("config_nonce"),
            &row.get::<String, _>("config_key_id"),
        )?,
        is_active: row.get("is_active"),
        sync_interval_minutes: row.get("sync_interval_minutes"),
        last_sync_at: row.get("last_sync_at"),
    })
}

fn validate_price_rule_request(payload: &PriceRuleRequest) -> Result<(), ApiError> {
    if payload.code.trim().is_empty() || payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request("Code und Name sind erforderlich"));
    }
    if !matches!(payload.source_type.trim(), "import" | "api" | "supplier") {
        return Err(ApiError::bad_request("Quellentyp ist ungültig"));
    }
    if !matches!(
        payload.rounding_mode.trim(),
        "none" | "cent" | "five_cent" | "ten_cent" | "whole"
    ) {
        return Err(ApiError::bad_request("Rundungsmodus ist ungültig"));
    }
    validate_number(&payload.markup_percent, "Aufschlag")?;
    let markup = payload
        .markup_percent
        .trim()
        .parse::<f64>()
        .map_err(|_| ApiError::bad_request("Aufschlag ist ungültig"))?;
    if !(-100.0..=1000.0).contains(&markup) {
        return Err(ApiError::bad_request(
            "Aufschlag muss zwischen -100 und 1000 Prozent liegen",
        ));
    }
    Ok(())
}

async fn write_price_rule(
    db: &PgPool,
    schema_name: &str,
    rule_id: Uuid,
    payload: &PriceRuleRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.price_rules (
                id, code, name, source_type, source_id, markup_percent, rounding_mode, is_active
            ) values ($1,$2,$3,$4,$5,$6::numeric,$7,$8)
            "#,
            schema = schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.price_rules
            set code=$2, name=$3, source_type=$4, source_id=$5,
                markup_percent=$6::numeric, rounding_mode=$7, is_active=$8,
                updated_at=now()
            where id=$1
            "#,
            schema = schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(rule_id)
        .bind(payload.code.trim())
        .bind(payload.name.trim())
        .bind(payload.source_type.trim())
        .bind(payload.source_id)
        .bind(payload.markup_percent.trim())
        .bind(payload.rounding_mode.trim())
        .bind(payload.is_active)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Preisregel nicht gefunden")?;
    }
    Ok(())
}

fn price_rule_response(id: Uuid, payload: PriceRuleRequest) -> PriceRuleResponse {
    PriceRuleResponse {
        id,
        code: payload.code,
        name: payload.name,
        source_type: payload.source_type,
        source_id: payload.source_id,
        markup_percent: payload.markup_percent,
        rounding_mode: payload.rounding_mode,
        is_active: payload.is_active,
    }
}

fn price_rule_from_row(row: sqlx::postgres::PgRow) -> PriceRuleResponse {
    PriceRuleResponse {
        id: row.get("id"),
        code: row.get("code"),
        name: row.get("name"),
        source_type: row.get("source_type"),
        source_id: row.get("source_id"),
        markup_percent: row.get("markup_percent"),
        rounding_mode: row.get("rounding_mode"),
        is_active: row.get("is_active"),
    }
}

fn validate_communication_request(payload: &CommunicationRequest) -> Result<(), ApiError> {
    validate_status_choice(
        &payload.communication_type,
        &["email", "phone", "letter", "meeting", "internal_note"],
        "Kommunikationstyp ist ungültig",
    )?;
    validate_status_choice(
        &payload.direction,
        &["inbound", "outbound", "internal"],
        "Richtung ist ungültig",
    )?;
    validate_status_choice(
        &payload.status,
        &["open", "done", "archived"],
        "Kommunikationsstatus ist ungültig",
    )?;
    if payload.subject.trim().len() < 2 {
        return Err(ApiError::bad_request("Betreff ist erforderlich"));
    }
    validate_entity_links(&payload.links, &communication_link_entity_types())
}

fn validate_document_upload_request(payload: &DocumentUploadRequest) -> Result<(), ApiError> {
    if payload.title.trim().len() < 2 {
        return Err(ApiError::bad_request("Dokumenttitel ist erforderlich"));
    }
    if payload.file_name.trim().len() < 2 {
        return Err(ApiError::bad_request("Dateiname ist erforderlich"));
    }
    if payload.content_type.trim().is_empty() {
        return Err(ApiError::bad_request("Content-Type ist erforderlich"));
    }
    validate_entity_links(&payload.links, &document_link_entity_types())
}

fn communication_link_entity_types() -> [&'static str; 8] {
    [
        "customer",
        "supplier",
        "activity",
        "quote",
        "outgoing_invoice",
        "incoming_invoice",
        "item",
        "document",
    ]
}

fn document_link_entity_types() -> [&'static str; 8] {
    [
        "customer",
        "supplier",
        "activity",
        "communication",
        "quote",
        "outgoing_invoice",
        "incoming_invoice",
        "item",
    ]
}

fn validate_entity_links(links: &[EntityLinkRequest], allowed: &[&str]) -> Result<(), ApiError> {
    for link in links {
        if !allowed.contains(&link.entity_type.as_str()) {
            return Err(ApiError::bad_request("Bezugstyp ist ungültig"));
        }
    }
    Ok(())
}

fn encrypted_optional_string(
    crypto: &DataCrypto,
    value: &str,
) -> Result<Option<crate::crypto_at_rest::EncryptedField>, ApiError> {
    if value.trim().is_empty() {
        Ok(None)
    } else {
        Ok(Some(crypto.encrypt(&value.trim().to_string())?))
    }
}

async fn write_communication(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    communication_id: Uuid,
    payload: &CommunicationRequest,
    insert: bool,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(&context.schema_name)?;
    let subject = crypto.encrypt(&payload.subject.trim().to_string())?;
    let body = encrypted_optional_string(crypto, &payload.body)?;
    let sql = if insert {
        format!(
            r#"
            insert into {schema}.communications (
                id, communication_type, direction, subject_ciphertext, subject_nonce,
                subject_key_id, body_ciphertext, body_nonce, body_key_id, status,
                occurred_at, created_by_user_id
            ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::timestamptz,$12)
            "#,
            schema = context.schema_name
        )
    } else {
        format!(
            r#"
            update {schema}.communications
            set communication_type=$2, direction=$3, subject_ciphertext=$4,
                subject_nonce=$5, subject_key_id=$6, body_ciphertext=$7,
                body_nonce=$8, body_key_id=$9, status=$10,
                occurred_at=$11::timestamptz, updated_at=now()
            where id=$1
            "#,
            schema = context.schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(communication_id)
        .bind(payload.communication_type.trim())
        .bind(payload.direction.trim())
        .bind(subject.ciphertext)
        .bind(subject.nonce)
        .bind(subject.key_id)
        .bind(body.as_ref().map(|value| value.ciphertext.clone()))
        .bind(body.as_ref().map(|value| value.nonce.clone()))
        .bind(body.as_ref().map(|value| value.key_id.clone()))
        .bind(payload.status.trim())
        .bind(payload.occurred_at.as_deref())
        .bind(context.user_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Kommunikation nicht gefunden")?;
    }
    write_communication_links(db, &context.schema_name, communication_id, &payload.links).await
}

async fn write_communication_links(
    db: &PgPool,
    schema_name: &str,
    communication_id: Uuid,
    links: &[EntityLinkRequest],
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let delete_sql = format!(
        "delete from {schema}.communication_links where communication_id=$1",
        schema = schema_name
    );
    sqlx::query(&delete_sql)
        .bind(communication_id)
        .execute(db)
        .await?;
    let insert_sql = format!(
        "insert into {schema}.communication_links (communication_id, entity_type, entity_id) values ($1,$2,$3) on conflict do nothing",
        schema = schema_name
    );
    for link in links {
        sqlx::query(&insert_sql)
            .bind(communication_id)
            .bind(link.entity_type.trim())
            .bind(link.entity_id)
            .execute(db)
            .await?;
    }
    Ok(())
}

async fn write_document_links(
    db: &PgPool,
    schema_name: &str,
    document_id: Uuid,
    links: &[EntityLinkRequest],
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let delete_sql = format!(
        "delete from {schema}.document_links where document_id=$1",
        schema = schema_name
    );
    sqlx::query(&delete_sql)
        .bind(document_id)
        .execute(db)
        .await?;
    let insert_sql = format!(
        "insert into {schema}.document_links (document_id, entity_type, entity_id) values ($1,$2,$3) on conflict do nothing",
        schema = schema_name
    );
    for link in links {
        sqlx::query(&insert_sql)
            .bind(document_id)
            .bind(link.entity_type.trim())
            .bind(link.entity_id)
            .execute(db)
            .await?;
    }
    Ok(())
}

async fn get_communication_by_id(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    communication_id: Uuid,
) -> Result<Json<CommunicationResponse>, ApiError> {
    let sql = format!(
        r#"
        select id, communication_type, direction, subject_ciphertext, subject_nonce,
               subject_key_id, body_ciphertext, body_nonce, body_key_id, status, occurred_at
        from {schema}.communications
        where id=$1
        "#,
        schema = schema_name
    );
    let row = sqlx::query(&sql)
        .bind(communication_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Kommunikation nicht gefunden"))?;
    Ok(Json(
        communication_from_row(db, crypto, schema_name, row).await?,
    ))
}

async fn communication_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
) -> Result<CommunicationResponse, ApiError> {
    let id: Uuid = row.get("id");
    Ok(CommunicationResponse {
        id,
        communication_type: row.get("communication_type"),
        direction: row.get("direction"),
        subject: crypto.decrypt(
            &row.get::<Vec<u8>, _>("subject_ciphertext"),
            &row.get::<Vec<u8>, _>("subject_nonce"),
            &row.get::<String, _>("subject_key_id"),
        )?,
        body: decrypt_optional_string(crypto, &row, "body")?.unwrap_or_default(),
        status: row.get("status"),
        occurred_at: row.get("occurred_at"),
        links: load_links(
            db,
            schema_name,
            "communication_links",
            "communication_id",
            id,
        )
        .await?,
    })
}

fn decrypt_optional_string(
    crypto: &DataCrypto,
    row: &sqlx::postgres::PgRow,
    prefix: &str,
) -> Result<Option<String>, ApiError> {
    let ciphertext_column = format!("{prefix}_ciphertext");
    let nonce_column = format!("{prefix}_nonce");
    let key_column = format!("{prefix}_key_id");
    let ciphertext = row.get::<Option<Vec<u8>>, _>(ciphertext_column.as_str());
    let nonce = row.get::<Option<Vec<u8>>, _>(nonce_column.as_str());
    let key_id = row.get::<Option<String>, _>(key_column.as_str());
    match (ciphertext, nonce, key_id) {
        (Some(ciphertext), Some(nonce), Some(key_id)) => {
            Ok(Some(crypto.decrypt(&ciphertext, &nonce, &key_id)?))
        }
        _ => Ok(None),
    }
}

async fn load_links(
    db: &PgPool,
    schema_name: &str,
    table: &str,
    id_column: &str,
    id: Uuid,
) -> Result<Vec<EntityLinkRequest>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        "select entity_type, entity_id from {schema}.{table} where {id_column}=$1 order by entity_type",
        schema = schema_name
    );
    let rows = sqlx::query(&sql).bind(id).fetch_all(db).await?;
    Ok(rows
        .into_iter()
        .map(|row| EntityLinkRequest {
            entity_type: row.get("entity_type"),
            entity_id: row.get("entity_id"),
        })
        .collect())
}

async fn get_document_by_id(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    document_id: Uuid,
) -> Result<Json<DocumentResponse>, ApiError> {
    let sql = format!(
        r#"
        select id, title_ciphertext, title_nonce, title_key_id,
               description_ciphertext, description_nonce, description_key_id, status
        from {schema}.documents
        where id=$1
        "#,
        schema = schema_name
    );
    let row = sqlx::query(&sql)
        .bind(document_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Dokument nicht gefunden"))?;
    Ok(Json(document_from_row(db, crypto, schema_name, row).await?))
}

async fn document_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
) -> Result<DocumentResponse, ApiError> {
    let id: Uuid = row.get("id");
    Ok(DocumentResponse {
        id,
        title: crypto.decrypt(
            &row.get::<Vec<u8>, _>("title_ciphertext"),
            &row.get::<Vec<u8>, _>("title_nonce"),
            &row.get::<String, _>("title_key_id"),
        )?,
        description: decrypt_optional_string(crypto, &row, "description")?.unwrap_or_default(),
        status: row.get("status"),
        latest_version: latest_document_version(db, crypto, schema_name, id).await?,
        links: load_links(db, schema_name, "document_links", "document_id", id).await?,
    })
}

async fn latest_document_version(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    document_id: Uuid,
) -> Result<Option<DocumentVersionResponse>, ApiError> {
    let sql = format!(
        r#"
        select id, version_no, file_name_ciphertext, file_name_nonce, file_name_key_id,
               content_type_ciphertext, content_type_nonce, content_type_key_id,
               file_size, checksum_sha256, created_at
        from {schema}.document_versions
        where document_id=$1
        order by version_no desc
        limit 1
        "#,
        schema = schema_name
    );
    let Some(row) = sqlx::query(&sql)
        .bind(document_id)
        .fetch_optional(db)
        .await?
    else {
        return Ok(None);
    };
    Ok(Some(DocumentVersionResponse {
        id: row.get("id"),
        version_no: row.get("version_no"),
        file_name: crypto.decrypt(
            &row.get::<Vec<u8>, _>("file_name_ciphertext"),
            &row.get::<Vec<u8>, _>("file_name_nonce"),
            &row.get::<String, _>("file_name_key_id"),
        )?,
        content_type: crypto.decrypt(
            &row.get::<Vec<u8>, _>("content_type_ciphertext"),
            &row.get::<Vec<u8>, _>("content_type_nonce"),
            &row.get::<String, _>("content_type_key_id"),
        )?,
        file_size: row.get("file_size"),
        checksum_sha256: row.get("checksum_sha256"),
        created_at: row.get("created_at"),
    }))
}

fn document_storage_path(
    schema_name: &str,
    document_id: Uuid,
    version_id: Uuid,
) -> Result<PathBuf, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let base = env::var("COMPANYTOOL_DOCUMENT_STORAGE_DIR")
        .unwrap_or_else(|_| "storage/documents".to_string());
    Ok(PathBuf::from(base)
        .join(schema_name)
        .join(document_id.to_string())
        .join(format!("{version_id}.json")))
}

async fn insert_document_audit(
    db: &PgPool,
    schema_name: &str,
    document_id: Uuid,
    version_id: Option<Uuid>,
    action: &str,
    user_id: Uuid,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        "insert into {schema}.document_audit_log (id, document_id, version_id, action, user_id) values ($1,$2,$3,$4,$5)",
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(Uuid::new_v4())
        .bind(document_id)
        .bind(version_id)
        .bind(action)
        .bind(user_id)
        .execute(db)
        .await?;
    Ok(())
}

fn validate_item_request(payload: &ItemRequest) -> Result<(), ApiError> {
    if payload.name.trim().len() < 2 {
        return Err(ApiError::bad_request("Artikelname ist erforderlich"));
    }
    validate_status(&payload.status)?;
    validate_tax_rate(&payload.tax_rate)?;
    if let Some(price) = &payload.default_purchase_price {
        validate_number(price, "Einkaufspreis")?;
    }
    if let Some(price) = &payload.default_sales_price {
        validate_number(price, "Verkaufspreis")?;
    }
    for supplier_price in &payload.supplier_prices {
        if supplier_price.external_item_number.trim().is_empty() {
            return Err(ApiError::bad_request(
                "Externe Lieferanten-Artikelnummer fehlt",
            ));
        }
        validate_number(&supplier_price.purchase_price, "Lieferantenpreis")?;
        if supplier_price.currency.trim().len() != 3 {
            return Err(ApiError::bad_request(
                "Währung muss aus drei Zeichen bestehen",
            ));
        }
    }
    Ok(())
}

async fn write_item(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    item_id: Uuid,
    payload: &ItemRequest,
    insert: bool,
) -> Result<(), ApiError> {
    let name = crypto.encrypt(&payload.name.trim().to_string())?;
    let sql = if insert {
        format!(
            r#"insert into {schema}.items (id,item_number,manufacturer_code,name_ciphertext,name_nonce,name_key_id,unit,tax_rate,default_purchase_price,default_sales_price,status)
            values ($1,$2,$3,$4,$5,$6,$7,$8::numeric,$9::numeric,$10::numeric,$11)"#,
            schema = schema_name
        )
    } else {
        format!(
            r#"update {schema}.items set item_number=item_number,manufacturer_code=$3,name_ciphertext=$4,name_nonce=$5,name_key_id=$6,unit=$7,tax_rate=$8::numeric,
            default_purchase_price=$9::numeric,default_sales_price=$10::numeric,status=$11,updated_at=now() where id=$1"#,
            schema = schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(item_id)
        .bind(payload.item_number.trim())
        .bind(
            payload
                .manufacturer_code
                .as_deref()
                .map(str::trim)
                .filter(|value| !value.is_empty()),
        )
        .bind(name.ciphertext)
        .bind(name.nonce)
        .bind(name.key_id)
        .bind(payload.unit.trim())
        .bind(normalize_number_string(&payload.tax_rate))
        .bind(normalize_optional_number(
            payload.default_purchase_price.as_deref(),
        ))
        .bind(normalize_optional_number(
            payload.default_sales_price.as_deref(),
        ))
        .bind(&payload.status)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Artikel nicht gefunden")?;
    }
    Ok(())
}

async fn item_from_row(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    row: sqlx::postgres::PgRow,
) -> Result<ItemResponse, ApiError> {
    let item_id = row.get("id");
    Ok(ItemResponse {
        id: item_id,
        item_number: row.get("item_number"),
        name: crypto.decrypt(
            &row.get::<Vec<u8>, _>("name_ciphertext"),
            &row.get::<Vec<u8>, _>("name_nonce"),
            &row.get::<String, _>("name_key_id"),
        )?,
        manufacturer_code: row.get("manufacturer_code"),
        unit: row.get("unit"),
        tax_rate: row.get("tax_rate"),
        default_purchase_price: row.get("default_purchase_price"),
        default_sales_price: row.get("default_sales_price"),
        status: row.get("status"),
        supplier_prices: load_item_supplier_prices(db, crypto, schema_name, item_id).await?,
    })
}

async fn load_item_response_by_id(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    item_id: Uuid,
) -> Result<ItemResponse, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select id, item_number, manufacturer_code, name_ciphertext, name_nonce, name_key_id, unit,
               tax_rate::text tax_rate, default_purchase_price::text default_purchase_price,
               default_sales_price::text default_sales_price, status
        from {schema}.items
        where id=$1
        "#,
        schema = schema_name
    );
    let row = sqlx::query(&sql)
        .bind(item_id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Artikel nicht gefunden"))?;
    item_from_row(db, crypto, schema_name, row).await
}

async fn load_item_supplier_prices(
    db: &PgPool,
    crypto: &DataCrypto,
    schema_name: &str,
    item_id: Uuid,
) -> Result<Vec<ItemSupplierPriceResponse>, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select p.id, p.supplier_id, s.supplier_number, s.name_ciphertext, s.name_nonce,
               s.name_key_id, p.external_item_number, p.purchase_price::text purchase_price,
               p.currency, p.is_preferred, p.valid_from::text valid_from,
               p.valid_until::text valid_until, p.source
        from {schema}.item_supplier_prices p
        join {schema}.suppliers s on s.id = p.supplier_id
        where p.item_id = $1
        order by p.purchase_price asc, s.supplier_number
        "#,
        schema = schema_name
    );
    let rows = sqlx::query(&sql).bind(item_id).fetch_all(db).await?;
    let mut prices = Vec::with_capacity(rows.len());
    for row in rows {
        prices.push(ItemSupplierPriceResponse {
            id: row.get("id"),
            supplier_id: row.get("supplier_id"),
            supplier_number: row.get("supplier_number"),
            supplier_name: crypto.decrypt(
                &row.get::<Vec<u8>, _>("name_ciphertext"),
                &row.get::<Vec<u8>, _>("name_nonce"),
                &row.get::<String, _>("name_key_id"),
            )?,
            external_item_number: row.get("external_item_number"),
            purchase_price: row.get("purchase_price"),
            currency: row.get("currency"),
            is_preferred: row.get("is_preferred"),
            valid_from: row.get("valid_from"),
            valid_until: row.get("valid_until"),
            source: row.get("source"),
        });
    }
    Ok(prices)
}

async fn save_item_supplier_prices(
    db: &PgPool,
    schema_name: &str,
    item_id: Uuid,
    payload: &ItemRequest,
    source: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let delete_sql = format!(
        "delete from {schema}.item_supplier_prices where item_id=$1",
        schema = schema_name
    );
    sqlx::query(&delete_sql).bind(item_id).execute(db).await?;
    for supplier_price in &payload.supplier_prices {
        ensure_supplier_exists(db, schema_name, supplier_price.supplier_id).await?;
        let insert_sql = format!(
            r#"
            insert into {schema}.item_supplier_prices (
                id, item_id, supplier_id, external_item_number, purchase_price, currency,
                is_preferred, valid_from, valid_until, source
            ) values ($1,$2,$3,$4,$5::numeric,$6,$7,$8::date,$9::date,$10)
            "#,
            schema = schema_name
        );
        sqlx::query(&insert_sql)
            .bind(Uuid::new_v4())
            .bind(item_id)
            .bind(supplier_price.supplier_id)
            .bind(supplier_price.external_item_number.trim())
            .bind(normalize_number_string(&supplier_price.purchase_price))
            .bind(supplier_price.currency.trim().to_uppercase())
            .bind(supplier_price.is_preferred)
            .bind(
                supplier_price
                    .valid_from
                    .as_deref()
                    .filter(|value| !value.is_empty()),
            )
            .bind(
                supplier_price
                    .valid_until
                    .as_deref()
                    .filter(|value| !value.is_empty()),
            )
            .bind(source)
            .execute(db)
            .await?;
    }
    Ok(())
}

async fn record_item_price_history(
    db: &PgPool,
    schema_name: &str,
    item_id: Uuid,
    payload: &ItemRequest,
    user_id: Uuid,
) -> Result<(), ApiError> {
    record_item_price_history_with_source(
        db,
        schema_name,
        item_id,
        payload.default_purchase_price.as_deref(),
        payload.default_sales_price.as_deref(),
        "manual",
        user_id,
    )
    .await
}

async fn record_item_price_history_with_source(
    db: &PgPool,
    schema_name: &str,
    item_id: Uuid,
    purchase_price: Option<&str>,
    sales_price: Option<&str>,
    source: &str,
    user_id: Uuid,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.item_price_history (
            id, item_id, purchase_price, sales_price, source, created_by_user_id
        ) values ($1, $2, $3::numeric, $4::numeric, $5, $6)
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(Uuid::new_v4())
        .bind(item_id)
        .bind(purchase_price)
        .bind(sales_price)
        .bind(source)
        .bind(user_id)
        .execute(db)
        .await?;
    Ok(())
}

fn item_price_history_from_row(row: sqlx::postgres::PgRow) -> ItemPriceHistoryResponse {
    ItemPriceHistoryResponse {
        id: row.get("id"),
        item_id: row.get("item_id"),
        purchase_price: row.get("purchase_price"),
        sales_price: row.get("sales_price"),
        source: row.get("source"),
        valid_from: row.get("valid_from"),
        created_by_user_id: row.get("created_by_user_id"),
        created_at: row.get("created_at"),
    }
}

fn validate_activity_request(payload: &ActivityRequest) -> Result<(), ApiError> {
    if payload.title.trim().len() < 2 {
        return Err(ApiError::bad_request("Titel der Aktivität ist zu kurz"));
    }
    if ![
        "email_note",
        "phone_note",
        "internal_note",
        "task",
        "follow_up",
        "calendar_event",
        "system_event",
        "work_step",
    ]
    .contains(&payload.activity_type.as_str())
    {
        return Err(ApiError::bad_request("Ungültiger Aktivitätstyp"));
    }
    if !["active", "inactive"].contains(&payload.status.as_str()) {
        return Err(ApiError::bad_request("Ungültiger Aktivitätsstatus"));
    }
    if !["low", "normal", "high", "critical"].contains(&payload.priority.as_str()) {
        return Err(ApiError::bad_request("Ungültige Priorität"));
    }
    Ok(())
}

async fn write_activity(
    db: &PgPool,
    crypto: &DataCrypto,
    context: &AuthContext,
    activity_id: Uuid,
    payload: &ActivityRequest,
    insert: bool,
) -> Result<(), ApiError> {
    let title = crypto.encrypt(&payload.title.trim().to_string())?;
    let body = if payload.body.trim().is_empty() {
        None
    } else {
        Some(crypto.encrypt(&payload.body)?)
    };
    let sql = if insert {
        format!(
            r#"insert into {schema}.activities (id,activity_number,activity_type,title_ciphertext,title_nonce,title_key_id,body_ciphertext,body_nonce,body_key_id,status,priority,due_at,created_by_user_id)
            values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)"#,
            schema = context.schema_name
        )
    } else {
        format!(
            r#"update {schema}.activities set activity_number=activity_number,activity_type=$3,title_ciphertext=$4,title_nonce=$5,title_key_id=$6,body_ciphertext=$7,body_nonce=$8,body_key_id=$9,status=$10,priority=$11,due_at=$12,created_by_user_id=coalesce(created_by_user_id,$13),updated_at=now() where id=$1"#,
            schema = context.schema_name
        )
    };
    let result = sqlx::query(&sql)
        .bind(activity_id)
        .bind(
            payload
                .activity_number
                .as_deref()
                .map(str::trim)
                .filter(|value| !value.is_empty()),
        )
        .bind(&payload.activity_type)
        .bind(title.ciphertext)
        .bind(title.nonce)
        .bind(title.key_id)
        .bind(body.as_ref().map(|field| field.ciphertext.clone()))
        .bind(body.as_ref().map(|field| field.nonce.clone()))
        .bind(body.as_ref().map(|field| field.key_id.clone()))
        .bind(&payload.status)
        .bind(&payload.priority)
        .bind(payload.due_at)
        .bind(context.user_id)
        .execute(db)
        .await?;
    if !insert {
        ensure_changed(result.rows_affected(), "Aktivität nicht gefunden")?;
    }
    Ok(())
}

fn activity_response(id: Uuid, payload: ActivityRequest) -> ActivityResponse {
    ActivityResponse {
        id,
        activity_number: payload.activity_number,
        activity_type: payload.activity_type,
        title: payload.title,
        body: payload.body,
        status: payload.status,
        priority: payload.priority,
        due_at: payload.due_at,
    }
}

fn activity_from_row(
    crypto: &DataCrypto,
    row: sqlx::postgres::PgRow,
) -> Result<ActivityResponse, ApiError> {
    let body = match row.get::<Option<Vec<u8>>, _>("body_ciphertext") {
        Some(ciphertext) => crypto.decrypt::<String>(
            &ciphertext,
            &row.get::<Vec<u8>, _>("body_nonce"),
            &row.get::<String, _>("body_key_id"),
        )?,
        None => String::new(),
    };
    Ok(ActivityResponse {
        id: row.get("id"),
        activity_number: row.get("activity_number"),
        activity_type: row.get("activity_type"),
        title: crypto.decrypt(
            &row.get::<Vec<u8>, _>("title_ciphertext"),
            &row.get::<Vec<u8>, _>("title_nonce"),
            &row.get::<String, _>("title_key_id"),
        )?,
        body,
        status: row.get("status"),
        priority: row.get("priority"),
        due_at: row.get("due_at"),
    })
}

async fn set_record_inactive(
    db: &PgPool,
    schema_name: &str,
    table: &str,
    id: Uuid,
) -> Result<(), ApiError> {
    let sql =
        format!("update {schema_name}.{table} set status='inactive', updated_at=now() where id=$1");
    let result = sqlx::query(&sql).bind(id).execute(db).await?;
    ensure_changed(result.rows_affected(), "Datensatz nicht gefunden")
}

fn ensure_changed(rows_affected: u64, message: &str) -> Result<(), ApiError> {
    if rows_affected == 0 {
        Err(ApiError::not_found(message))
    } else {
        Ok(())
    }
}

fn validate_status(status: &str) -> Result<(), ApiError> {
    if ["active", "inactive", "blocked"].contains(&status) {
        Ok(())
    } else {
        Err(ApiError::bad_request("Ungültiger Status"))
    }
}

fn validate_status_choice(value: &str, allowed: &[&str], message: &str) -> Result<(), ApiError> {
    if allowed.contains(&value) {
        Ok(())
    } else {
        Err(ApiError::bad_request(message))
    }
}

fn validate_number(value: &str, field: &str) -> Result<(), ApiError> {
    let parsed = parse_number(value)
        .map_err(|_| ApiError::bad_request(&format!("Ungültiger Wert: {field}")))?;
    if parsed < 0.0 {
        Err(ApiError::bad_request(&format!(
            "{field} darf nicht negativ sein"
        )))
    } else {
        Ok(())
    }
}

fn validate_tax_rate(value: &str) -> Result<(), ApiError> {
    let normalized = normalize_number_string(value);
    match normalized.as_str() {
        "0" | "0.0" | "0.00" | "7" | "7.0" | "7.00" | "19" | "19.0" | "19.00" => Ok(()),
        _ => Err(ApiError::bad_request("Steuersatz muss 0, 7 oder 19 % sein")),
    }
}

fn validate_percent(value: &str, field: &str) -> Result<(), ApiError> {
    let parsed = parse_number(value)
        .map_err(|_| ApiError::bad_request(&format!("Ungültiger Wert: {field}")))?;
    if (0.0..=100.0).contains(&parsed) {
        Ok(())
    } else {
        Err(ApiError::bad_request(&format!(
            "{field} muss zwischen 0 und 100 liegen"
        )))
    }
}

fn parse_number(value: &str) -> Result<f64, std::num::ParseFloatError> {
    normalize_number_string(value).parse::<f64>()
}

fn normalize_number_string(value: &str) -> String {
    let trimmed = value.trim().replace(' ', "");
    if trimmed.contains(',') {
        trimmed.replace('.', "").replace(',', ".")
    } else if trimmed.matches('.').count() > 1 {
        trimmed.replace('.', "")
    } else if is_german_thousands_without_decimal(&trimmed) {
        trimmed.replace('.', "")
    } else {
        trimmed
    }
}

fn is_german_thousands_without_decimal(value: &str) -> bool {
    let unsigned = value.strip_prefix('-').unwrap_or(value);
    let Some((head, tail)) = unsigned.split_once('.') else {
        return false;
    };
    !head.is_empty()
        && head.len() <= 3
        && head.chars().all(|character| character.is_ascii_digit())
        && tail.len() == 3
        && tail.chars().all(|character| character.is_ascii_digit())
}

fn normalize_optional_number(value: Option<&str>) -> Option<String> {
    value
        .map(normalize_number_string)
        .filter(|value| !value.trim().is_empty())
}

fn validate_number_range(payload: &NumberRangeRequest) -> Result<(), ApiError> {
    if !payload.pattern.contains("{counter}") {
        return Err(ApiError::bad_request(
            "Muster muss den Platzhalter {counter} enthalten",
        ));
    }
    if payload.counter_value < 0 {
        return Err(ApiError::bad_request("Zähler darf nicht negativ sein"));
    }
    if !(1..=18).contains(&payload.counter_padding) {
        return Err(ApiError::bad_request(
            "Zählerlänge muss zwischen 1 und 18 liegen",
        ));
    }
    Ok(())
}

fn number_range_from_row(row: sqlx::postgres::PgRow) -> NumberRangeResponse {
    NumberRangeResponse {
        id: row.get("id"),
        code: row.get("code"),
        pattern: row.get("pattern"),
        counter_value: row.get("counter_value"),
        counter_padding: row.get("counter_padding"),
        reset_rule: row.get("reset_rule"),
        is_active: row.get("is_active"),
    }
}

async fn next_number_if_blank(
    db: &PgPool,
    schema_name: &str,
    code: &str,
    current_value: &str,
) -> Result<String, ApiError> {
    if current_value.trim().is_empty() {
        next_number(db, schema_name, code).await
    } else {
        Ok(current_value.trim().to_string())
    }
}

fn ensure_known_number_range_code(code: &str) -> Result<(), ApiError> {
    if [
        "customers",
        "suppliers",
        "items",
        "activities",
        "quotes",
        "outgoing_invoices",
        "incoming_invoices",
        "documents",
        "price_lists",
        "communications",
    ]
    .contains(&code)
    {
        Ok(())
    } else {
        Err(ApiError::bad_request("Unbekannter Nummernkreis"))
    }
}

fn number_range_write_permission(code: &str) -> &'static str {
    match code {
        "customers" => "customers.write",
        "suppliers" => "suppliers.write",
        "items" => "items.write",
        "activities" => "activities.write",
        "quotes" => "quotes.write",
        "outgoing_invoices" => "outgoing_invoices.write",
        "incoming_invoices" => "incoming_invoices.write",
        "documents" => "documents.write",
        "price_lists" => "price_lists.write",
        "communications" => "communications.write",
        _ => "number_ranges.write",
    }
}

async fn next_number(db: &PgPool, schema_name: &str, code: &str) -> Result<String, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        update {schema}.number_ranges
        set counter_value = counter_value + 1,
            updated_at = now()
        where code = $1 and is_active = true
        returning pattern, counter_value, counter_padding
        "#,
        schema = schema_name
    );
    let row = sqlx::query(&sql)
        .bind(code)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::bad_request("Nummernkreis fehlt oder ist inaktiv"))?;
    Ok(format_number(
        row.get("pattern"),
        row.get("counter_value"),
        row.get("counter_padding"),
    ))
}

fn format_number(pattern: String, counter_value: i64, counter_padding: i32) -> String {
    let padded = format!(
        "{:0width$}",
        counter_value,
        width = counter_padding.max(1) as usize
    );
    let grouped = padded
        .as_bytes()
        .rchunks(3)
        .rev()
        .map(|chunk| std::str::from_utf8(chunk).unwrap_or_default())
        .collect::<Vec<_>>()
        .join(".");
    pattern.replace("{counter}", &grouped)
}

async fn load_registration_detail(
    state: &AppState,
    id: Uuid,
) -> Result<OrganizationRegistrationDetail, ApiError> {
    let db = state.db()?;
    let row = sqlx::query(
        r#"
        select r.id, r.organization_name_ciphertext, r.organization_name_nonce,
               r.organization_name_key_id, r.email, r.status, r.organization_id,
               r.requested_at, r.decided_by_user_id, r.decided_at, r.decision_note,
               o.schema_name
        from organization_registration_requests r
        left join organizations o on o.id = r.organization_id
        where r.id = $1
        "#,
    )
    .bind(id)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::not_found("Registrierung nicht gefunden"))?;

    Ok(OrganizationRegistrationDetail {
        id: row.get("id"),
        organization_name: decrypt_string(
            &state.crypto,
            row.get("organization_name_ciphertext"),
            row.get("organization_name_nonce"),
            row.get("organization_name_key_id"),
        )?,
        email: row.get("email"),
        status: row.get("status"),
        organization_id: row.get("organization_id"),
        schema_name: row.get("schema_name"),
        requested_at: row.get("requested_at"),
        decided_at: row.get("decided_at"),
        decided_by_user_id: row.get("decided_by_user_id"),
        decision_note: row.get("decision_note"),
        provisioning_error: None,
    })
}

async fn load_registration_row(db: &PgPool, id: Uuid) -> Result<sqlx::postgres::PgRow, ApiError> {
    sqlx::query("select * from organization_registration_requests where id = $1")
        .bind(id)
        .fetch_optional(db)
        .await?
        .ok_or_else(|| ApiError::not_found("Registrierung nicht gefunden"))
}

async fn ensure_user(db: &PgPool, email: &str) -> Result<Uuid, ApiError> {
    if let Some(id) = sqlx::query_scalar::<_, Uuid>("select id from users where email = $1")
        .bind(email)
        .fetch_optional(db)
        .await?
    {
        return Ok(id);
    }

    let id = Uuid::new_v4();
    sqlx::query("insert into users (id, email) values ($1, $2)")
        .bind(id)
        .bind(email)
        .execute(db)
        .await?;
    Ok(id)
}

#[derive(Debug, Clone)]
struct AuthContext {
    user_id: Uuid,
    organization_id: Uuid,
    schema_name: String,
}

async fn create_session_token(
    db: &PgPool,
    user_id: Uuid,
    organization_id: Option<Uuid>,
) -> Result<String, ApiError> {
    let token = generate_token();
    let token_hash = hash_token(&token);
    sqlx::query(
        r#"
        insert into refresh_tokens (id, user_id, organization_id, token_hash, expires_at)
        values ($1, $2, $3, $4, now() + interval '30 days')
        "#,
    )
    .bind(Uuid::new_v4())
    .bind(user_id)
    .bind(organization_id)
    .bind(token_hash)
    .execute(db)
    .await?;
    Ok(token)
}

async fn require_auth(db: &PgPool, headers: &HeaderMap) -> Result<AuthContext, ApiError> {
    let token_hash = bearer_token(headers)
        .map(hash_token)
        .ok_or_else(|| ApiError::unauthorized("Anmeldung erforderlich"))?;
    let row = sqlx::query(
        r#"
        select rt.user_id, rt.organization_id, o.schema_name
        from refresh_tokens rt
        join organizations o on o.id = rt.organization_id
        join user_organizations uo on uo.organization_id = o.id and uo.user_id = rt.user_id
        where rt.token_hash = $1
          and rt.revoked_at is null
          and rt.expires_at > now()
          and o.status = 'active'
          and uo.status = 'active'
        limit 1
        "#,
    )
    .bind(token_hash)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::unauthorized("Session ist ungültig oder abgelaufen"))?;

    let user_id: Uuid = row.get("user_id");
    let organization_id = row
        .get::<Option<Uuid>, _>("organization_id")
        .ok_or_else(|| ApiError::bad_request("Keine Firma ausgewählt"))?;
    let schema_name = row
        .get::<Option<String>, _>("schema_name")
        .unwrap_or_else(|| company_schema_name(organization_id));

    Ok(AuthContext {
        user_id,
        organization_id,
        schema_name,
    })
}

async fn require_permission(
    db: &PgPool,
    headers: &HeaderMap,
    permission_code: &str,
) -> Result<AuthContext, ApiError> {
    let context = require_auth(db, headers).await?;
    if user_has_permission(db, &context.schema_name, context.user_id, permission_code).await? {
        Ok(context)
    } else {
        Err(ApiError::forbidden("Berechtigung fehlt"))
    }
}

async fn load_context_for_user_and_organization(
    db: &PgPool,
    user_id: Uuid,
    organization_id: Uuid,
) -> Result<AuthContext, ApiError> {
    let row = sqlx::query(
        r#"
        select o.id organization_id, o.schema_name
        from organizations o
        join user_organizations uo on uo.organization_id = o.id
        where o.status = 'active'
          and uo.status = 'active'
          and uo.user_id = $1
          and o.id = $2
        limit 1
        "#,
    )
    .bind(user_id)
    .bind(organization_id)
    .fetch_optional(db)
    .await?
    .ok_or_else(|| ApiError::forbidden("Kein Zugriff auf diese Firma"))?;

    Ok(AuthContext {
        user_id,
        organization_id: row.get("organization_id"),
        schema_name: row
            .get::<Option<String>, _>("schema_name")
            .unwrap_or_else(|| company_schema_name(organization_id)),
    })
}

async fn update_session_organization(
    db: &PgPool,
    headers: &HeaderMap,
    organization_id: Uuid,
) -> Result<(), ApiError> {
    let token_hash = bearer_token(headers)
        .map(hash_token)
        .ok_or_else(|| ApiError::unauthorized("Anmeldung erforderlich"))?;
    sqlx::query("update refresh_tokens set organization_id = $2 where token_hash = $1")
        .bind(token_hash)
        .bind(organization_id)
        .execute(db)
        .await?;
    Ok(())
}

fn bearer_token(headers: &HeaderMap) -> Option<&str> {
    headers
        .get("authorization")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .filter(|value| !value.trim().is_empty())
}

fn emit_change(state: &AppState, title: &str) {
    let _ = state.events.send(ServerMessage::RecordChanged {
        record: RecordSummary {
            id: Uuid::new_v4(),
            title: title.to_string(),
            updated_at: Utc::now(),
        },
    });
}

async fn enqueue_initial_password_email(
    crypto: &DataCrypto,
    db: &PgPool,
    email: &str,
    password: &str,
) -> Result<(), ApiError> {
    enqueue_email(
        crypto,
        db,
        email,
        "initial_password",
        "Company Tool Initialpasswort",
        json!({
        "email": email,
        "initial_password": password
        }),
    )
    .await
}

async fn enqueue_password_reset_email(
    crypto: &DataCrypto,
    db: &PgPool,
    email: &str,
    token: &str,
) -> Result<(), ApiError> {
    enqueue_email(
        crypto,
        db,
        email,
        "password_reset",
        "Company Tool Passwort zurücksetzen",
        json!({
            "email": email,
            "reset_token": token
        }),
    )
    .await
}

async fn enqueue_invitation_email(
    crypto: &DataCrypto,
    db: &PgPool,
    email: &str,
    token: &str,
    organization_id: Uuid,
) -> Result<(), ApiError> {
    enqueue_email(
        crypto,
        db,
        email,
        "invitation",
        "Company Tool Einladung",
        json!({
            "email": email,
            "invitation_token": token,
            "organization_id": organization_id
        }),
    )
    .await
}

async fn enqueue_email(
    crypto: &DataCrypto,
    db: &PgPool,
    email: &str,
    template: &str,
    subject: &str,
    payload_value: serde_json::Value,
) -> Result<(), ApiError> {
    let payload = crypto.encrypt(&payload_value)?;
    let email_id = Uuid::new_v4();
    sqlx::query(
        r#"
        insert into email_outbox (
            id, recipient_email, template, subject, payload_ciphertext, payload_nonce, payload_key_id
        ) values ($1, $2, $3, $4, $5, $6, $7)
        "#,
    )
    .bind(email_id)
    .bind(email)
    .bind(template)
    .bind(subject)
    .bind(&payload.ciphertext)
    .bind(&payload.nonce)
    .bind(&payload.key_id)
    .execute(db)
    .await?;
    deliver_email_if_configured(db, email_id, email, template, subject, &payload_value).await?;
    Ok(())
}

async fn deliver_email_if_configured(
    db: &PgPool,
    email_id: Uuid,
    email: &str,
    template: &str,
    subject: &str,
    payload: &serde_json::Value,
) -> Result<(), ApiError> {
    let transport = env::var("COMPANYTOOL_EMAIL_TRANSPORT").unwrap_or_else(|_| {
        if dev_mode_enabled() {
            "dev_outbox".to_string()
        } else {
            "outbox".to_string()
        }
    });
    match transport.as_str() {
        "file" | "dev_file" => {
            let dir = env::var("COMPANYTOOL_EMAIL_FILE_DIR")
                .unwrap_or_else(|_| "storage/email-outbox".to_string());
            tokio::fs::create_dir_all(&dir).await?;
            let path = PathBuf::from(dir).join(format!("{email_id}.json"));
            tokio::fs::write(
                path,
                serde_json::to_vec_pretty(&json!({
                    "id": email_id,
                    "recipient_email": email,
                    "template": template,
                    "subject": subject,
                    "payload": payload,
                    "created_at": Utc::now()
                }))?,
            )
            .await?;
            sqlx::query(
                "update email_outbox set status='sent', delivered_via=$2, sent_at=now() where id=$1",
            )
            .bind(email_id)
            .bind(transport)
            .execute(db)
            .await?;
        }
        "outbox" | "dev_outbox" => {}
        other => {
            sqlx::query(
                "update email_outbox set status='failed', last_error=$2, attempt_count=attempt_count+1 where id=$1",
            )
            .bind(email_id)
            .bind(format!("Unbekannter E-Mail-Transport: {other}"))
            .execute(db)
            .await?;
        }
    }
    Ok(())
}

async fn provision_company_schema(db: &PgPool, schema_name: &str) -> Result<(), ApiError> {
    let mut tx = db.begin().await?;
    provision_company_schema_tx(&mut tx, schema_name).await?;
    tx.commit().await?;
    Ok(())
}

async fn provision_company_schema_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    sqlx::query(&format!("create schema if not exists {schema_name}"))
        .execute(&mut **tx)
        .await?;

    for template in [
        include_str!("../company-migrations/0001_company_base.sql"),
        include_str!("../company-migrations/0002_activity_price_invoice_rules.sql"),
        include_str!("../company-migrations/0003_customer_details.sql"),
        include_str!("../company-migrations/0004_item_price_history.sql"),
        include_str!("../company-migrations/0005_numbered_activities.sql"),
        include_str!("../company-migrations/0006_update_number_range_prefixes.sql"),
        include_str!("../company-migrations/0007_quotes.sql"),
        include_str!("../company-migrations/0008_invoice_links.sql"),
        include_str!("../company-migrations/0009_price_imports.sql"),
        include_str!("../company-migrations/0010_communications_documents.sql"),
        include_str!("../company-migrations/0011_user_settings.sql"),
        include_str!("../company-migrations/0012_item_supplier_prices.sql"),
        include_str!("../company-migrations/0013_item_unit_default.sql"),
        include_str!("../company-migrations/0014_activity_line_sources.sql"),
        include_str!("../company-migrations/0015_activity_status_active_inactive.sql"),
    ] {
        let sql = template
            .lines()
            .filter(|line| !line.trim_start().starts_with("--"))
            .collect::<Vec<_>>()
            .join("\n")
            .replace("{schema}", schema_name);
        for statement in split_sql_statements(&sql) {
            let statement = statement.trim();
            if !statement.is_empty() && !statement.starts_with("--") {
                sqlx::query(statement).execute(&mut **tx).await?;
            }
        }
    }

    seed_roles_and_permissions_tx(tx, schema_name).await?;
    seed_number_ranges_tx(tx, schema_name).await?;
    seed_import_mappings_tx(tx, schema_name).await?;
    Ok(())
}

fn default_user_navigation_settings() -> UserNavigationSettings {
    UserNavigationSettings {
        mode: "scroll".to_string(),
    }
}

fn normalize_user_navigation_settings(
    settings: UserNavigationSettings,
) -> Result<UserNavigationSettings, ApiError> {
    let mode = match settings.mode.trim().to_lowercase().as_str() {
        "scroll" | "compact" => "scroll".to_string(),
        "groups" => "groups".to_string(),
        _ => {
            return Err(ApiError::bad_request("Ungültige Navigationseinstellung"));
        }
    };
    Ok(UserNavigationSettings { mode })
}

fn split_sql_statements(sql: &str) -> Vec<String> {
    let mut statements = Vec::new();
    let mut current = String::new();
    let mut in_single_quote = false;
    let mut chars = sql.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '\'' {
            current.push(ch);
            if in_single_quote && chars.peek() == Some(&'\'') {
                current.push(chars.next().unwrap_or('\''));
            } else {
                in_single_quote = !in_single_quote;
            }
        } else if ch == ';' && !in_single_quote {
            statements.push(current.trim().to_string());
            current.clear();
        } else {
            current.push(ch);
        }
    }

    if !current.trim().is_empty() {
        statements.push(current.trim().to_string());
    }

    statements
}

pub async fn sync_all_company_schemas(db: &PgPool) -> anyhow::Result<()> {
    let rows = sqlx::query("select id, schema_name from organizations where status = 'active'")
        .fetch_all(db)
        .await?;

    for row in rows {
        let organization_id: Uuid = row.get("id");
        let schema_name = row
            .get::<Option<String>, _>("schema_name")
            .unwrap_or_else(|| company_schema_name(organization_id));
        provision_company_schema(db, &schema_name)
            .await
            .map_err(|error| anyhow::anyhow!(error.message))?;
    }

    Ok(())
}

async fn seed_roles_and_permissions_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    for (code, name) in ROLE_SEEDS {
        let sql = format!(
            "insert into {schema}.roles (id, code, name, is_system_role) values ($1, $2, $3, true) on conflict (code) do nothing",
            schema = schema_name
        );
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(code)
            .bind(name)
            .execute(&mut **tx)
            .await?;
    }

    for code in PERMISSION_SEEDS {
        let sql = format!(
            "insert into {schema}.permissions (id, code) values ($1, $2) on conflict (code) do nothing",
            schema = schema_name
        );
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(code)
            .execute(&mut **tx)
            .await?;
    }

    assign_role_permissions_tx(tx, schema_name).await?;
    Ok(())
}

async fn seed_number_ranges_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    for (code, pattern) in NUMBER_RANGE_SEEDS {
        let sql = format!(
            r#"
            insert into {schema}.number_ranges (
                id, code, pattern, counter_value, counter_padding, reset_rule, is_active
            ) values ($1, $2, $3, 0, 9, null, true)
            on conflict (code) do nothing
            "#,
            schema = schema_name
        );
        sqlx::query(&sql)
            .bind(Uuid::new_v4())
            .bind(code)
            .bind(pattern)
            .execute(&mut **tx)
            .await?;
    }
    Ok(())
}

async fn seed_import_mappings_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.import_mappings (
            id, code, name, delimiter, item_number_column, name_column, unit_column,
            tax_rate_column, purchase_price_column, sales_price_column, is_default
        ) values (
            $1, 'default_price_list_csv', 'Standard-Preislisten-CSV', ';',
            'item_number', 'name', 'unit', 'tax_rate', 'purchase_price', 'sales_price', true
        )
        on conflict (code) do nothing
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(Uuid::new_v4())
        .execute(&mut **tx)
        .await?;
    Ok(())
}

const NUMBER_RANGE_SEEDS: &[(&str, &str)] = &[
    ("items", "AR{counter}"),
    ("activities", "AK{counter}"),
    ("outgoing_invoices", "AR{counter}"),
    ("incoming_invoices", "ER{counter}"),
    ("customers", "KU{counter}"),
    ("suppliers", "LI{counter}"),
    ("quotes", "AN{counter}"),
];

const ROLE_SEEDS: &[(&str, &str)] = &[
    ("owner", "Besitzer"),
    ("admin", "Admin"),
    ("sales", "Vertrieb"),
    ("accounting", "Buchhaltung"),
    ("viewer", "Lesen"),
];

const PERMISSION_SEEDS: &[&str] = &[
    "users.read",
    "users.invite",
    "users.roles.write",
    "users.disable",
    "settings.read",
    "settings.write",
    "number_ranges.read",
    "number_ranges.write",
    "customers.read",
    "customers.write",
    "customers.delete",
    "customer_discounts.write",
    "suppliers.read",
    "suppliers.write",
    "suppliers.delete",
    "supplier_cash_discounts.write",
    "cash_discount_terms.read",
    "cash_discount_terms.write",
    "cash_discount_terms.delete",
    "items.read",
    "items.write",
    "items.delete",
    "item_prices.write",
    "price_lists.import",
    "price_apis.sync",
    "price_rules.write",
    "quotes.read",
    "quotes.write",
    "quotes.delete",
    "quotes.convert_to_invoice",
    "outgoing_invoices.read",
    "outgoing_invoices.write",
    "outgoing_invoices.delete",
    "invoices.finalize",
    "incoming_invoices.read",
    "incoming_invoices.write",
    "incoming_invoices.delete",
    "activities.read",
    "activities.write",
    "activities.delete",
    "communication.read",
    "communication.write",
    "documents.read",
    "documents.write",
    "reports.read",
    "audit_log.read",
];

const ADMIN_PERMISSIONS: &[&str] = &[
    "users.read",
    "users.invite",
    "settings.read",
    "settings.write",
    "number_ranges.read",
    "number_ranges.write",
    "customers.read",
    "customers.write",
    "customers.delete",
    "customer_discounts.write",
    "suppliers.read",
    "suppliers.write",
    "suppliers.delete",
    "supplier_cash_discounts.write",
    "cash_discount_terms.read",
    "cash_discount_terms.write",
    "cash_discount_terms.delete",
    "items.read",
    "items.write",
    "items.delete",
    "item_prices.write",
    "price_lists.import",
    "price_apis.sync",
    "price_rules.write",
    "quotes.read",
    "quotes.write",
    "quotes.delete",
    "quotes.convert_to_invoice",
    "outgoing_invoices.read",
    "outgoing_invoices.write",
    "outgoing_invoices.delete",
    "invoices.finalize",
    "incoming_invoices.read",
    "incoming_invoices.write",
    "incoming_invoices.delete",
    "activities.read",
    "activities.write",
    "activities.delete",
    "communication.read",
    "communication.write",
    "documents.read",
    "documents.write",
    "reports.read",
    "audit_log.read",
];

const SALES_PERMISSIONS: &[&str] = &[
    "customers.read",
    "customers.write",
    "customer_discounts.write",
    "cash_discount_terms.read",
    "items.read",
    "item_prices.write",
    "quotes.read",
    "quotes.write",
    "quotes.convert_to_invoice",
    "outgoing_invoices.read",
    "outgoing_invoices.write",
    "invoices.finalize",
    "activities.read",
    "activities.write",
    "communication.read",
    "communication.write",
    "documents.read",
    "documents.write",
];

const ACCOUNTING_PERMISSIONS: &[&str] = &[
    "customers.read",
    "suppliers.read",
    "items.read",
    "quotes.read",
    "outgoing_invoices.read",
    "outgoing_invoices.write",
    "invoices.finalize",
    "incoming_invoices.read",
    "incoming_invoices.write",
    "customer_discounts.write",
    "supplier_cash_discounts.write",
    "cash_discount_terms.read",
    "activities.read",
    "activities.write",
    "communication.read",
    "documents.read",
    "documents.write",
    "reports.read",
];

const VIEWER_PERMISSIONS: &[&str] = &[
    "settings.read",
    "customers.read",
    "suppliers.read",
    "cash_discount_terms.read",
    "items.read",
    "quotes.read",
    "outgoing_invoices.read",
    "incoming_invoices.read",
    "activities.read",
    "communication.read",
    "documents.read",
    "reports.read",
];

async fn assign_role_permissions_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
) -> Result<(), ApiError> {
    for (role, permissions) in [
        ("owner", PERMISSION_SEEDS),
        ("admin", ADMIN_PERMISSIONS),
        ("sales", SALES_PERMISSIONS),
        ("accounting", ACCOUNTING_PERMISSIONS),
        ("viewer", VIEWER_PERMISSIONS),
    ] {
        for permission in permissions {
            let sql = format!(
                r#"
                insert into {schema}.role_permissions (role_id, permission_id)
                select r.id, p.id
                from {schema}.roles r
                cross join {schema}.permissions p
                where r.code = $1 and p.code = $2
                on conflict (role_id, permission_id) do nothing
                "#,
                schema = schema_name
            );
            sqlx::query(&sql)
                .bind(role)
                .bind(permission)
                .execute(&mut **tx)
                .await?;
        }
    }
    Ok(())
}

async fn assign_role(
    db: &PgPool,
    schema_name: &str,
    user_id: Uuid,
    role_code: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.user_roles (user_id, role_id)
        select $1, id from {schema}.roles where code = $2
        on conflict (user_id, role_id) do nothing
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(user_id)
        .bind(role_code)
        .execute(db)
        .await?;
    Ok(())
}

async fn assign_role_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
    user_id: Uuid,
    role_code: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        insert into {schema}.user_roles (user_id, role_id)
        select $1, id from {schema}.roles where code = $2
        on conflict (user_id, role_id) do nothing
        "#,
        schema = schema_name
    );
    sqlx::query(&sql)
        .bind(user_id)
        .bind(role_code)
        .execute(&mut **tx)
        .await?;
    Ok(())
}

async fn assign_all_roles_tx(
    tx: &mut Transaction<'_, Postgres>,
    schema_name: &str,
    user_id: Uuid,
) -> Result<(), ApiError> {
    for role in ["owner", "admin", "sales", "accounting", "viewer"] {
        assign_role_tx(tx, schema_name, user_id, role).await?;
    }
    Ok(())
}

async fn replace_user_roles(
    db: &PgPool,
    schema_name: &str,
    user_id: Uuid,
    roles: &[String],
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    for role in roles {
        ensure_role_exists(db, schema_name, role).await?;
    }

    let delete_sql = format!(
        "delete from {schema}.user_roles where user_id = $1",
        schema = schema_name
    );
    sqlx::query(&delete_sql).bind(user_id).execute(db).await?;

    for role in roles {
        assign_role(db, schema_name, user_id, role).await?;
    }
    Ok(())
}

async fn ensure_role_exists(
    db: &PgPool,
    schema_name: &str,
    role_code: &str,
) -> Result<(), ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        "select id from {schema}.roles where code = $1 limit 1",
        schema = schema_name
    );
    let exists: Option<Uuid> = sqlx::query_scalar(&sql)
        .bind(role_code)
        .fetch_optional(db)
        .await?;

    if exists.is_some() {
        Ok(())
    } else {
        Err(ApiError::bad_request(&format!(
            "Unbekannte Rolle: {role_code}"
        )))
    }
}

async fn require_owner(db: &PgPool, schema_name: &str, user_id: Uuid) -> Result<(), ApiError> {
    if user_has_role(db, schema_name, user_id, "owner").await? {
        Ok(())
    } else {
        Err(ApiError::forbidden(
            "Nur der Besitzer darf Benutzerrechte verwalten",
        ))
    }
}

async fn user_has_role(
    db: &PgPool,
    schema_name: &str,
    user_id: Uuid,
    role_code: &str,
) -> Result<bool, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select exists(
            select 1
            from {schema}.roles r
            join {schema}.user_roles ur on ur.role_id = r.id
            where ur.user_id = $1 and r.code = $2
        )
        "#,
        schema = schema_name
    );
    let has_role = sqlx::query_scalar::<_, bool>(&sql)
        .bind(user_id)
        .bind(role_code)
        .fetch_one(db)
        .await?;
    Ok(has_role)
}

async fn user_has_permission(
    db: &PgPool,
    schema_name: &str,
    user_id: Uuid,
    permission_code: &str,
) -> Result<bool, ApiError> {
    ensure_safe_schema_name(schema_name)?;
    let sql = format!(
        r#"
        select exists(
            select 1
            from {schema}.user_roles ur
            join {schema}.role_permissions rp on rp.role_id = ur.role_id
            join {schema}.permissions p on p.id = rp.permission_id
            where ur.user_id = $1 and p.code = $2
        )
        "#,
        schema = schema_name
    );
    let has_permission = sqlx::query_scalar::<_, bool>(&sql)
        .bind(user_id)
        .bind(permission_code)
        .fetch_one(db)
        .await?;
    Ok(has_permission)
}

fn company_schema_name(organization_id: Uuid) -> String {
    format!("company_{}", organization_id.simple())
}

fn ensure_safe_schema_name(schema_name: &str) -> Result<(), ApiError> {
    if schema_name
        .chars()
        .all(|char| char.is_ascii_lowercase() || char.is_ascii_digit() || char == '_')
        && schema_name.starts_with("company_")
    {
        Ok(())
    } else {
        Err(ApiError::bad_request("Ungültiger Schema-Name"))
    }
}

fn decrypt_string(
    crypto: &DataCrypto,
    ciphertext: Vec<u8>,
    nonce: Vec<u8>,
    key_id: String,
) -> Result<String, ApiError> {
    Ok(crypto.decrypt(&ciphertext, &nonce, &key_id)?)
}

fn normalize_email(email: &str) -> Result<String, ApiError> {
    let email = email.trim().to_lowercase();
    if email.contains('@') && email.len() >= 5 {
        Ok(email)
    } else {
        Err(ApiError::bad_request("Ungültige E-Mail-Adresse"))
    }
}

fn validate_new_password(password: &str, password_confirm: &str) -> Result<(), ApiError> {
    if password != password_confirm {
        return Err(ApiError::bad_request(
            "Neue Passwörter stimmen nicht überein",
        ));
    }
    if password.len() < 12 {
        return Err(ApiError::bad_request(
            "Neues Passwort muss mindestens 12 Zeichen lang sein",
        ));
    }
    if !password.chars().any(|ch| ch.is_ascii_lowercase())
        || !password.chars().any(|ch| ch.is_ascii_uppercase())
        || !password.chars().any(|ch| ch.is_ascii_digit())
    {
        return Err(ApiError::bad_request(
            "Neues Passwort muss Kleinbuchstaben, Großbuchstaben und Zahlen enthalten",
        ));
    }
    Ok(())
}

fn dev_mode_enabled() -> bool {
    cfg!(debug_assertions) || env::var("COMPANYTOOL_DEV_MODE").as_deref() == Ok("1")
}

fn generate_initial_password() -> String {
    let mut bytes = [0_u8; 18];
    rand_core::OsRng.fill_bytes(&mut bytes);
    base64_url(&bytes)
}

fn generate_token() -> String {
    let mut bytes = [0_u8; 32];
    rand_core::OsRng.fill_bytes(&mut bytes);
    base64_url(&bytes)
}

fn hash_token(token: &str) -> String {
    let digest = Sha256::digest(token.as_bytes());
    base64_url(&digest)
}

fn base64_url(bytes: &[u8]) -> String {
    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
    URL_SAFE_NO_PAD.encode(bytes)
}

fn hash_password(password: &str) -> Result<String, ApiError> {
    let salt = SaltString::generate(&mut OsRng);
    Ok(Argon2::default()
        .hash_password(password.as_bytes(), &salt)
        .map_err(|error| ApiError::internal(error.to_string()))?
        .to_string())
}

fn verify_password(password: &str, hash: &str) -> Result<(), ApiError> {
    let parsed_hash =
        PasswordHash::new(hash).map_err(|_| ApiError::unauthorized("Login fehlgeschlagen"))?;
    Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .map_err(|_| ApiError::unauthorized("Login fehlgeschlagen"))
}

pub struct ApiError {
    status: StatusCode,
    message: String,
}

impl ApiError {
    pub(crate) fn bad_request(message: &str) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            message: message.to_string(),
        }
    }

    fn unauthorized(message: &str) -> Self {
        Self {
            status: StatusCode::UNAUTHORIZED,
            message: message.to_string(),
        }
    }

    fn forbidden(message: &str) -> Self {
        Self {
            status: StatusCode::FORBIDDEN,
            message: message.to_string(),
        }
    }

    fn not_found(message: &str) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            message: message.to_string(),
        }
    }

    fn conflict(message: &str) -> Self {
        Self {
            status: StatusCode::CONFLICT,
            message: message.to_string(),
        }
    }

    fn internal(message: String) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            message,
        }
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (self.status, Json(json!({ "message": self.message }))).into_response()
    }
}

impl From<sqlx::Error> for ApiError {
    fn from(error: sqlx::Error) -> Self {
        Self::internal(error.to_string())
    }
}

impl From<anyhow::Error> for ApiError {
    fn from(error: anyhow::Error) -> Self {
        Self::internal(error.to_string())
    }
}

impl From<std::io::Error> for ApiError {
    fn from(error: std::io::Error) -> Self {
        Self::internal(error.to_string())
    }
}

impl From<serde_json::Error> for ApiError {
    fn from(error: serde_json::Error) -> Self {
        Self::internal(error.to_string())
    }
}
