//! Billing routes for Stripe integration.

use axum::{
    body::Bytes,
    extract::State,
    http::{HeaderMap, StatusCode},
    routing::{get, post},
    Json, Router,
};
{{#if has_organizations}}
use sea_orm::EntityTrait;
{{/if}}
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tideway::billing::{
    BillingStore, CheckoutManager, CheckoutRequest as TidewayCheckoutRequest, InvoiceManager,
    PlanStore, PortalManager, StoredPlan, WebhookHandler, WebhookOutcome,
    validate_plan, validate_plan_with_stripe,
};
use tideway::{Result, TidewayError};

{{#if has_organizations}}
use crate::entities::organization;
{{/if}}
use crate::auth::RequestActor;
use crate::error::BillingState;

/// Public billing routes for pricing-safe data.
pub fn public_billing_routes() -> Router<Arc<BillingState>> {
    Router::new()
        .route("/plans", get(list_plans))
}

/// Authenticated billing routes for customer billing actions.
pub fn authenticated_billing_routes() -> Router<Arc<BillingState>> {
    Router::new()
        .route("/checkout", post(create_checkout))
        .route("/subscription", get(get_subscription))
        .route("/invoices", get(list_invoices))
        .route("/portal", post(create_portal))
}

/// Stripe webhook routes. These are public network paths but signature-verified.
pub fn billing_webhook_routes() -> Router<Arc<BillingState>> {
    Router::new().route("/stripe", post(handle_webhook))
}

/// Legacy combined billing router.
///
/// New scaffolds mount `public_billing_routes`, `authenticated_billing_routes`, and
/// `billing_webhook_routes` separately so the access boundary is visible in main.rs.
pub fn billing_routes() -> Router<Arc<BillingState>> {
    Router::new()
        .merge(public_billing_routes())
        .merge(authenticated_billing_routes())
        .nest("/webhook", billing_webhook_routes())
}

// Request/Response types

#[derive(Debug, Deserialize)]
pub struct CheckoutRequest {
    {{#if has_organizations}}
    pub organization_id: String,
    {{else}}
    pub user_id: String,
    {{/if}}
    pub success_url: String,
    pub cancel_url: String,
}

#[derive(Debug, Serialize)]
pub struct CheckoutResponse {
    pub checkout_url: String,
    pub session_id: String,
}

#[derive(Debug, Deserialize)]
pub struct SubscriptionRequest {
    {{#if has_organizations}}
    pub organization_id: String,
    {{else}}
    pub user_id: String,
    {{/if}}
}

#[derive(Debug, Serialize)]
pub struct SubscriptionResponse {
    pub has_subscription: bool,
    pub status: Option<String>,
    pub plan_id: Option<String>,
    pub trial_days_remaining: Option<u32>,
    pub cancel_at_period_end: bool,
}

#[derive(Debug, Deserialize)]
pub struct PortalRequest {
    {{#if has_organizations}}
    pub organization_id: String,
    {{else}}
    pub user_id: String,
    {{/if}}
    pub return_url: String,
}

#[derive(Debug, Serialize)]
pub struct PortalResponse {
    pub portal_url: String,
}

#[derive(Debug, Deserialize)]
pub struct InvoicesRequest {
    {{#if has_organizations}}
    pub organization_id: String,
    {{else}}
    pub user_id: String,
    {{/if}}
}

#[derive(Debug, Serialize)]
pub struct InvoiceResponse {
    pub id: String,
    pub status: String,
    pub amount_due: i64,
    pub amount_paid: i64,
    pub currency: String,
    pub created: u64,
    pub invoice_pdf: Option<String>,
    pub hosted_invoice_url: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct InvoicesListResponse {
    pub invoices: Vec<InvoiceResponse>,
}

#[derive(Debug, Serialize)]
pub struct PlanResponse {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub price_cents: i64,
    pub currency: String,
    pub interval: String,
    pub included_seats: u32,
    pub features: serde_json::Value,
    pub limits: serde_json::Value,
    pub trial_days: Option<u32>,
}

impl From<StoredPlan> for PlanResponse {
    fn from(plan: StoredPlan) -> Self {
        Self {
            id: plan.id,
            name: plan.name,
            description: plan.description,
            price_cents: plan.price_cents,
            currency: plan.currency,
            interval: plan.interval.as_str().to_string(),
            included_seats: plan.included_seats,
            features: plan.features,
            limits: plan.limits,
            trial_days: plan.trial_days,
        }
    }
}

#[derive(Debug, Serialize)]
pub struct PlansListResponse {
    pub plans: Vec<PlanResponse>,
}

// Handlers

/// List available plans (public endpoint for pricing pages).
async fn list_plans(
    State(state): State<Arc<BillingState>>,
) -> Result<Json<PlansListResponse>> {
    let plans = state.billing_store.list_plans().await?;
    let plans = plans.into_iter().map(PlanResponse::from).collect();
    Ok(Json(PlansListResponse { plans }))
}

async fn create_checkout(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    Json(body): Json<CheckoutRequest>,
) -> Result<Json<CheckoutResponse>> {
    {{#if has_organizations}}
    authorize_billing_owner(&headers, &state, &body.organization_id).await?;

    let org = organization::Entity::find_by_id(&body.organization_id)
        .one(state.db.as_ref())
        .await
        .map_err(|e| tideway::TidewayError::Database(e.to_string()))?
        .ok_or_else(|| tideway::TidewayError::NotFound("Organization not found".to_string()))?;

    let checkout_manager = CheckoutManager::new(
        state.billing_store.clone(),
        state.stripe_client.clone(),
        state.plans.clone(),
        state.checkout_config.clone(),
    );

    let request = TidewayCheckoutRequest::new(
        "default",
        body.success_url,
        body.cancel_url,
    );

    let session = checkout_manager.create_checkout_session(&org, request).await?;
    {{else}}
    authorize_billing_owner(&headers, &state, &body.user_id).await?;

    // For B2C, create a simple billable entity
    let billable = SimpleBillable {
        id: body.user_id.clone(),
        email: "".to_string(), // Would be fetched from user
    };

    let checkout_manager = CheckoutManager::new(
        state.billing_store.clone(),
        state.stripe_client.clone(),
        state.plans.clone(),
        state.checkout_config.clone(),
    );

    let request = TidewayCheckoutRequest::new(
        "default",
        body.success_url,
        body.cancel_url,
    );

    let session = checkout_manager.create_checkout_session(&billable, request).await?;
    {{/if}}

    Ok(Json(CheckoutResponse {
        checkout_url: session.url,
        session_id: session.id,
    }))
}

async fn get_subscription(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Query(params): axum::extract::Query<SubscriptionRequest>,
) -> Result<Json<SubscriptionResponse>> {
    {{#if has_organizations}}
    let billable_id = &params.organization_id;
    {{else}}
    let billable_id = &params.user_id;
    {{/if}}
    authorize_billing_member(&headers, &state, billable_id).await?;

    let subscription = state.billing_store
        .get_subscription(billable_id)
        .await?;

    match subscription {
        Some(sub) => {
            Ok(Json(SubscriptionResponse {
                has_subscription: true,
                status: Some(sub.status.as_str().to_string()),
                plan_id: Some(sub.plan_id.clone()),
                trial_days_remaining: sub.trial_days_remaining(),
                cancel_at_period_end: sub.cancel_at_period_end,
            }))
        }
        None => {
            Ok(Json(SubscriptionResponse {
                has_subscription: false,
                status: None,
                plan_id: None,
                trial_days_remaining: None,
                cancel_at_period_end: false,
            }))
        }
    }
}

async fn list_invoices(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Query(params): axum::extract::Query<InvoicesRequest>,
) -> Result<Json<InvoicesListResponse>> {
    {{#if has_organizations}}
    let billable_id = &params.organization_id;
    {{else}}
    let billable_id = &params.user_id;
    {{/if}}
    authorize_billing_member(&headers, &state, billable_id).await?;

    let invoice_manager = InvoiceManager::new(
        state.billing_store.clone(),
        state.stripe_client.clone(),
    );

    let invoice_list = invoice_manager
        .list_invoices(billable_id, Default::default())
        .await?;

    let invoices = invoice_list
        .invoices
        .into_iter()
        .map(|inv| InvoiceResponse {
            id: inv.id,
            status: inv.status.as_str().to_string(),
            amount_due: inv.amount_due,
            amount_paid: inv.amount_paid,
            currency: inv.currency,
            created: inv.created,
            invoice_pdf: inv.invoice_pdf,
            hosted_invoice_url: inv.hosted_invoice_url,
        })
        .collect();

    Ok(Json(InvoicesListResponse { invoices }))
}

async fn create_portal(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    Json(body): Json<PortalRequest>,
) -> Result<Json<PortalResponse>> {
    {{#if has_organizations}}
    authorize_billing_owner(&headers, &state, &body.organization_id).await?;

    let _org = organization::Entity::find_by_id(&body.organization_id)
        .one(state.db.as_ref())
        .await
        .map_err(|e| tideway::TidewayError::Database(e.to_string()))?
        .ok_or_else(|| tideway::TidewayError::NotFound("Organization not found".to_string()))?;

    let billable_id = &body.organization_id;
    {{else}}
    authorize_billing_owner(&headers, &state, &body.user_id).await?;

    let billable_id = &body.user_id;
    {{/if}}

    let portal_manager = PortalManager::new(
        state.billing_store.clone(),
        state.stripe_client.clone(),
        state.portal_config.clone(),
    );

    let session = portal_manager.create_portal_session(billable_id, &body.return_url).await?;

    Ok(Json(PortalResponse {
        portal_url: session.url,
    }))
}

async fn authorize_billing_member(
    headers: &HeaderMap,
    state: &Arc<BillingState>,
    billable_id: &str,
) -> Result<()> {
    {{#if has_organizations}}
    let _actor = RequestActor::for_organization(headers, state.db.as_ref(), billable_id).await?;
    {{else}}
    let actor = RequestActor::from_headers(headers, state.db.as_ref()).await?;
    if actor.user.id.to_string() != billable_id {
        return Err(TidewayError::Forbidden("Billing resource access denied".into()));
    }
    {{/if}}
    Ok(())
}

async fn authorize_billing_owner(
    headers: &HeaderMap,
    state: &Arc<BillingState>,
    billable_id: &str,
) -> Result<()> {
    {{#if has_organizations}}
    let actor = RequestActor::for_organization(headers, state.db.as_ref(), billable_id).await?;
    actor.require_org_role(&["owner", "admin"])?;
    {{else}}
    let actor = RequestActor::from_headers(headers, state.db.as_ref()).await?;
    if actor.user.id.to_string() != billable_id {
        return Err(TidewayError::Forbidden("Billing resource access denied".into()));
    }
    {{/if}}
    Ok(())
}

async fn handle_webhook(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    body: Bytes,
) -> std::result::Result<StatusCode, (StatusCode, String)> {
    let signature = headers
        .get("stripe-signature")
        .and_then(|v| v.to_str().ok())
        .ok_or_else(|| {
            (StatusCode::BAD_REQUEST, "Missing Stripe-Signature header".to_string())
        })?;

    let webhook_handler = WebhookHandler::new(
        state.billing_store.clone(),
        state.webhook_secret.clone(),
        state.plans.clone(),
    );

    let event = webhook_handler
        .verify_signature(&body, signature)
        .map_err(|e| {
            tracing::warn!(error = %e, "Webhook signature verification failed");
            (StatusCode::BAD_REQUEST, "Invalid signature".to_string())
        })?;

    tracing::info!(
        event_type = %event.event_type,
        event_id = %event.id,
        "Processing Stripe webhook"
    );

    // Handle price events for plan sync
    if event.event_type.starts_with("price.") {
        return handle_price_webhook(&state, &event).await;
    }

    match webhook_handler.handle_event(event).await {
        Ok(outcome) => {
            match outcome {
                WebhookOutcome::Processed => {
                    tracing::info!("Webhook processed successfully");
                }
                WebhookOutcome::AlreadyProcessed => {
                    tracing::info!("Webhook already processed");
                }
                WebhookOutcome::Ignored => {
                    tracing::debug!("Webhook ignored");
                }
            }
            Ok(StatusCode::OK)
        }
        Err(e) => {
            tracing::error!(error = %e, "Failed to process webhook");
            Err((StatusCode::INTERNAL_SERVER_ERROR, "Processing failed".to_string()))
        }
    }
}

/// Handle Stripe price events to sync plan status.
///
/// When a price is archived or deleted in Stripe, deactivates any plans using that price.
async fn handle_price_webhook(
    state: &Arc<BillingState>,
    event: &tideway::billing::WebhookEvent,
) -> std::result::Result<StatusCode, (StatusCode, String)> {
    let price_id = event.data.object.get("id")
        .and_then(|v| v.as_str())
        .unwrap_or("");

    let is_active = event.data.object.get("active")
        .and_then(|v| v.as_bool())
        .unwrap_or(true);

    match event.event_type.as_str() {
        "price.updated" if !is_active => {
            // Price was archived - deactivate plans using it
            tracing::warn!(
                price_id = %price_id,
                "Stripe price was archived, checking for affected plans"
            );
            deactivate_plans_with_price(state, price_id).await;
        }
        "price.deleted" => {
            // Price was deleted - deactivate plans using it
            tracing::warn!(
                price_id = %price_id,
                "Stripe price was deleted, checking for affected plans"
            );
            deactivate_plans_with_price(state, price_id).await;
        }
        _ => {
            tracing::debug!(event_type = %event.event_type, "Price event ignored");
        }
    }

    Ok(StatusCode::OK)
}

/// Deactivate plans that use a specific Stripe price.
async fn deactivate_plans_with_price(state: &Arc<BillingState>, price_id: &str) {
    // Find plans using this price
    match state.billing_store.list_all_plans().await {
        Ok(plans) => {
            for plan in plans {
                let uses_price = plan.stripe_price_id == price_id
                    || plan.stripe_seat_price_id.as_deref() == Some(price_id);

                if uses_price && plan.is_active {
                    tracing::warn!(
                        plan_id = %plan.id,
                        price_id = %price_id,
                        "Deactivating plan due to archived/deleted Stripe price"
                    );

                    if let Err(e) = state.billing_store.set_plan_active(&plan.id, false).await {
                        tracing::error!(
                            plan_id = %plan.id,
                            error = %e,
                            "Failed to deactivate plan"
                        );
                    }
                }
            }
        }
        Err(e) => {
            tracing::error!(error = %e, "Failed to list plans for price sync");
        }
    }
}

{{#unless has_organizations}}
/// Simple billable entity for B2C billing.
struct SimpleBillable {
    id: String,
    email: String,
}

impl tideway::billing::BillableEntity for SimpleBillable {
    fn billable_id(&self) -> &str {
        &self.id
    }

    fn billable_type(&self) -> &str {
        "user"
    }

    fn email(&self) -> &str {
        &self.email
    }

    fn name(&self) -> Option<&str> {
        None
    }
}
{{/unless}}

// =============================================================================
// Admin Plan Management Routes
// =============================================================================

use axum::routing::{delete, put};
use tideway::billing::PlanInterval;

/// Admin plan management routes (requires admin authentication).
pub fn admin_billing_routes() -> Router<Arc<BillingState>> {
    Router::new()
        .route("/plans", get(admin_list_plans))
        .route("/plans", post(admin_create_plan))
        .route("/plans/export", get(admin_export_plans))
        .route("/plans/import", post(admin_import_plans))
        .route("/plans/{plan_id}", get(admin_get_plan))
        .route("/plans/{plan_id}", put(admin_update_plan))
        .route("/plans/{plan_id}", delete(admin_delete_plan))
        .route("/plans/{plan_id}/active", post(admin_set_plan_active))
}

/// Legacy name for admin billing routes.
pub fn admin_plan_routes() -> Router<Arc<BillingState>> {
    admin_billing_routes()
}

// Admin Plan Request/Response Types

#[derive(Debug, Serialize)]
pub struct AdminPlanResponse {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub stripe_price_id: String,
    pub stripe_seat_price_id: Option<String>,
    pub price_cents: i64,
    pub currency: String,
    pub interval: String,
    pub included_seats: u32,
    pub features: serde_json::Value,
    pub limits: serde_json::Value,
    pub trial_days: Option<u32>,
    pub is_active: bool,
    pub sort_order: i32,
    pub created_at: u64,
    pub updated_at: u64,
}

impl From<StoredPlan> for AdminPlanResponse {
    fn from(plan: StoredPlan) -> Self {
        Self {
            id: plan.id,
            name: plan.name,
            description: plan.description,
            stripe_price_id: plan.stripe_price_id,
            stripe_seat_price_id: plan.stripe_seat_price_id,
            price_cents: plan.price_cents,
            currency: plan.currency,
            interval: plan.interval.as_str().to_string(),
            included_seats: plan.included_seats,
            features: plan.features,
            limits: plan.limits,
            trial_days: plan.trial_days,
            is_active: plan.is_active,
            sort_order: plan.sort_order,
            created_at: plan.created_at,
            updated_at: plan.updated_at,
        }
    }
}

#[derive(Debug, Serialize)]
pub struct AdminPlansListResponse {
    pub plans: Vec<AdminPlanResponse>,
}

#[derive(Debug, Deserialize)]
pub struct CreatePlanRequest {
    pub id: String,
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    pub stripe_price_id: String,
    #[serde(default)]
    pub stripe_seat_price_id: Option<String>,
    pub price_cents: i64,
    #[serde(default = "default_currency")]
    pub currency: String,
    #[serde(default = "default_interval")]
    pub interval: String,
    #[serde(default = "default_included_seats")]
    pub included_seats: u32,
    #[serde(default = "default_features")]
    pub features: serde_json::Value,
    #[serde(default = "default_limits")]
    pub limits: serde_json::Value,
    #[serde(default)]
    pub trial_days: Option<u32>,
    #[serde(default = "default_is_active")]
    pub is_active: bool,
    #[serde(default)]
    pub sort_order: i32,
    /// If true, validate that the Stripe price ID exists before creating.
    #[serde(default)]
    pub validate_stripe: bool,
}

fn default_currency() -> String { "usd".to_string() }
fn default_interval() -> String { "monthly".to_string() }
fn default_included_seats() -> u32 { 1 }
fn default_features() -> serde_json::Value { serde_json::json!({}) }
fn default_limits() -> serde_json::Value { serde_json::json!({}) }
fn default_is_active() -> bool { true }

#[derive(Debug, Deserialize)]
pub struct UpdatePlanRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    pub stripe_price_id: Option<String>,
    pub stripe_seat_price_id: Option<String>,
    pub price_cents: Option<i64>,
    pub currency: Option<String>,
    pub interval: Option<String>,
    pub included_seats: Option<u32>,
    pub features: Option<serde_json::Value>,
    pub limits: Option<serde_json::Value>,
    pub trial_days: Option<u32>,
    pub is_active: Option<bool>,
    pub sort_order: Option<i32>,
}

#[derive(Debug, Deserialize)]
pub struct SetPlanActiveRequest {
    pub is_active: bool,
}

/// Plan export format.
#[derive(Debug, Serialize, Deserialize)]
pub struct PlanExport {
    pub version: String,
    pub exported_at: u64,
    pub plans: Vec<ExportedPlan>,
}

/// Exported plan format (excludes timestamps, includes all config).
#[derive(Debug, Serialize, Deserialize)]
pub struct ExportedPlan {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub stripe_price_id: String,
    pub stripe_seat_price_id: Option<String>,
    pub price_cents: i64,
    pub currency: String,
    pub interval: String,
    pub included_seats: u32,
    pub features: serde_json::Value,
    pub limits: serde_json::Value,
    pub trial_days: Option<u32>,
    pub is_active: bool,
    pub sort_order: i32,
}

impl From<StoredPlan> for ExportedPlan {
    fn from(plan: StoredPlan) -> Self {
        Self {
            id: plan.id,
            name: plan.name,
            description: plan.description,
            stripe_price_id: plan.stripe_price_id,
            stripe_seat_price_id: plan.stripe_seat_price_id,
            price_cents: plan.price_cents,
            currency: plan.currency,
            interval: plan.interval.as_str().to_string(),
            included_seats: plan.included_seats,
            features: plan.features,
            limits: plan.limits,
            trial_days: plan.trial_days,
            is_active: plan.is_active,
            sort_order: plan.sort_order,
        }
    }
}

/// Import plan request with conflict resolution strategy.
#[derive(Debug, Deserialize)]
pub struct ImportPlansRequest {
    pub plans: Vec<ExportedPlan>,
    /// How to handle existing plans: "skip", "update", or "error" (default)
    #[serde(default = "default_conflict_strategy")]
    pub on_conflict: String,
    /// Whether to validate Stripe prices
    #[serde(default)]
    pub validate_stripe: bool,
}

fn default_conflict_strategy() -> String { "error".to_string() }

/// Import result.
#[derive(Debug, Serialize)]
pub struct ImportPlansResponse {
    pub created: u32,
    pub updated: u32,
    pub skipped: u32,
    pub errors: Vec<ImportError>,
}

#[derive(Debug, Serialize)]
pub struct ImportError {
    pub plan_id: String,
    pub error: String,
}

// Admin Plan Handlers

async fn require_platform_admin(headers: &HeaderMap, state: &Arc<BillingState>) -> Result<()> {
    let actor = RequestActor::from_headers(headers, state.db.as_ref()).await?;
    actor.require_admin()
}

/// List all plans (including inactive) for admin.
async fn admin_list_plans(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
) -> Result<Json<AdminPlansListResponse>> {
    require_platform_admin(&headers, &state).await?;
    let plans = state.billing_store.list_all_plans().await?;
    let plans = plans.into_iter().map(AdminPlanResponse::from).collect();
    Ok(Json(AdminPlansListResponse { plans }))
}

/// Get a single plan by ID.
async fn admin_get_plan(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Path(plan_id): axum::extract::Path<String>,
) -> Result<Json<AdminPlanResponse>> {
    require_platform_admin(&headers, &state).await?;
    let plan = state.billing_store
        .get_plan(&plan_id)
        .await?
        .ok_or_else(|| tideway::TidewayError::NotFound(format!("Plan '{}' not found", plan_id)))?;
    Ok(Json(AdminPlanResponse::from(plan)))
}

/// Create a new plan.
///
/// If `validate_stripe` is true in the request, validates that the Stripe price ID
/// exists and is active before creating the plan.
///
/// This endpoint is idempotent: if a plan with the same ID already exists and
/// has matching stripe_price_id, returns the existing plan. If the plan exists
/// but has different configuration, returns 409 Conflict.
async fn admin_create_plan(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    Json(body): Json<CreatePlanRequest>,
) -> Result<Json<AdminPlanResponse>> {
    require_platform_admin(&headers, &state).await?;
    // Idempotency check: if plan already exists with same ID
    if let Some(existing) = state.billing_store.get_plan(&body.id).await? {
        // If stripe_price_id matches, this is likely a retry - return existing
        if existing.stripe_price_id == body.stripe_price_id {
            tracing::info!(plan_id = %body.id, "Plan already exists (idempotent retry)");
            return Ok(Json(AdminPlanResponse::from(existing)));
        }
        // Different configuration = conflict
        return Err(tideway::TidewayError::Conflict(
            format!("Plan '{}' already exists with different configuration", body.id)
        ));
    }

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    let plan = StoredPlan {
        id: body.id,
        name: body.name,
        description: body.description,
        stripe_price_id: body.stripe_price_id,
        stripe_seat_price_id: body.stripe_seat_price_id,
        price_cents: body.price_cents,
        currency: body.currency,
        interval: PlanInterval::from_str(&body.interval),
        included_seats: body.included_seats,
        features: body.features,
        limits: body.limits,
        trial_days: body.trial_days,
        is_active: body.is_active,
        sort_order: body.sort_order,
        created_at: now,
        updated_at: now,
    };

    // Validate plan data (format, required fields, etc.)
    validate_plan(&plan)?;

    // Optionally validate against Stripe API
    if body.validate_stripe {
        validate_plan_with_stripe(&plan, &state.stripe_client).await?;
    }

    state.billing_store.create_plan(&plan).await?;

    tracing::info!(plan_id = %plan.id, "Created billing plan");
    Ok(Json(AdminPlanResponse::from(plan)))
}

/// Update an existing plan.
async fn admin_update_plan(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Path(plan_id): axum::extract::Path<String>,
    Json(body): Json<UpdatePlanRequest>,
) -> Result<Json<AdminPlanResponse>> {
    require_platform_admin(&headers, &state).await?;
    let mut plan = state.billing_store
        .get_plan(&plan_id)
        .await?
        .ok_or_else(|| tideway::TidewayError::NotFound(format!("Plan '{}' not found", plan_id)))?;

    // Track changes for audit
    let mut changes = Vec::new();

    // Apply updates
    if let Some(name) = body.name {
        changes.push(format!("name: {} -> {}", plan.name, name));
        plan.name = name;
    }
    if let Some(desc) = body.description {
        changes.push("description".to_string());
        plan.description = Some(desc);
    }
    if let Some(price_id) = body.stripe_price_id {
        changes.push(format!("stripe_price_id: {} -> {}", plan.stripe_price_id, price_id));
        plan.stripe_price_id = price_id;
    }
    if let Some(seat_price_id) = body.stripe_seat_price_id {
        changes.push("stripe_seat_price_id".to_string());
        plan.stripe_seat_price_id = Some(seat_price_id);
    }
    if let Some(price) = body.price_cents {
        changes.push(format!("price_cents: {} -> {}", plan.price_cents, price));
        plan.price_cents = price;
    }
    if let Some(currency) = body.currency {
        changes.push(format!("currency: {} -> {}", plan.currency, currency));
        plan.currency = currency;
    }
    if let Some(interval) = body.interval {
        changes.push("interval".to_string());
        plan.interval = PlanInterval::from_str(&interval);
    }
    if let Some(seats) = body.included_seats {
        changes.push(format!("included_seats: {} -> {}", plan.included_seats, seats));
        plan.included_seats = seats;
    }
    if let Some(features) = body.features {
        changes.push("features".to_string());
        plan.features = features;
    }
    if let Some(limits) = body.limits {
        changes.push("limits".to_string());
        plan.limits = limits;
    }
    if let Some(trial) = body.trial_days {
        changes.push("trial_days".to_string());
        plan.trial_days = Some(trial);
    }
    if let Some(active) = body.is_active {
        changes.push(format!("is_active: {} -> {}", plan.is_active, active));
        plan.is_active = active;
    }
    if let Some(order) = body.sort_order {
        changes.push(format!("sort_order: {} -> {}", plan.sort_order, order));
        plan.sort_order = order;
    }

    plan.updated_at = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    state.billing_store.update_plan(&plan).await?;

    tracing::info!(plan_id = %plan.id, changes = ?changes, "Updated billing plan");
    Ok(Json(AdminPlanResponse::from(plan)))
}

/// Delete a plan.
///
/// This will fail if there are active subscriptions using this plan.
/// Consider using `set_plan_active(false)` to deactivate instead.
async fn admin_delete_plan(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Path(plan_id): axum::extract::Path<String>,
) -> Result<Json<tideway::MessageResponse>> {
    require_platform_admin(&headers, &state).await?;
    // Check plan exists
    let _ = state.billing_store
        .get_plan(&plan_id)
        .await?
        .ok_or_else(|| tideway::TidewayError::NotFound(format!("Plan '{}' not found", plan_id)))?;

    // Check for active subscriptions using this plan
    let subscription_count = state.billing_store
        .count_subscriptions_by_plan(&plan_id)
        .await?;

    if subscription_count > 0 {
        return Err(tideway::billing::BillingError::PlanHasActiveSubscriptions {
            plan_id: plan_id.clone(),
            subscription_count,
        }.into());
    }

    state.billing_store.delete_plan(&plan_id).await?;

    tracing::info!(plan_id = %plan_id, "Deleted billing plan");
    Ok(Json(tideway::MessageResponse::success("Plan deleted successfully")))
}

/// Set plan active/inactive status.
async fn admin_set_plan_active(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    axum::extract::Path(plan_id): axum::extract::Path<String>,
    Json(body): Json<SetPlanActiveRequest>,
) -> Result<Json<AdminPlanResponse>> {
    require_platform_admin(&headers, &state).await?;
    // Check plan exists
    let _ = state.billing_store
        .get_plan(&plan_id)
        .await?
        .ok_or_else(|| tideway::TidewayError::NotFound(format!("Plan '{}' not found", plan_id)))?;

    state.billing_store.set_plan_active(&plan_id, body.is_active).await?;

    // Fetch updated plan
    let plan = state.billing_store.get_plan(&plan_id).await?.unwrap();

    tracing::info!(plan_id = %plan_id, is_active = body.is_active, "Updated plan active status");
    Ok(Json(AdminPlanResponse::from(plan)))
}

/// Export all plans as JSON.
///
/// Returns plans in a format suitable for importing into another environment.
async fn admin_export_plans(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
) -> Result<Json<PlanExport>> {
    require_platform_admin(&headers, &state).await?;
    let plans = state.billing_store.list_all_plans().await?;
    let exported: Vec<ExportedPlan> = plans.into_iter().map(ExportedPlan::from).collect();

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    tracing::info!(count = exported.len(), "Exported billing plans");

    Ok(Json(PlanExport {
        version: "1.0".to_string(),
        exported_at: now,
        plans: exported,
    }))
}

/// Import plans from JSON.
///
/// Supports three conflict resolution strategies:
/// - "error" (default): Fail if any plan already exists
/// - "skip": Skip plans that already exist
/// - "update": Update existing plans with imported data
async fn admin_import_plans(
    State(state): State<Arc<BillingState>>,
    headers: HeaderMap,
    Json(body): Json<ImportPlansRequest>,
) -> Result<Json<ImportPlansResponse>> {
    require_platform_admin(&headers, &state).await?;
    let mut created = 0u32;
    let mut updated = 0u32;
    let mut skipped = 0u32;
    let mut errors: Vec<ImportError> = Vec::new();

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);

    for exported in body.plans {
        let plan_id = exported.id.clone();

        // Convert to StoredPlan
        let plan = StoredPlan {
            id: exported.id,
            name: exported.name,
            description: exported.description,
            stripe_price_id: exported.stripe_price_id,
            stripe_seat_price_id: exported.stripe_seat_price_id,
            price_cents: exported.price_cents,
            currency: exported.currency,
            interval: PlanInterval::from_str(&exported.interval),
            included_seats: exported.included_seats,
            features: exported.features,
            limits: exported.limits,
            trial_days: exported.trial_days,
            is_active: exported.is_active,
            sort_order: exported.sort_order,
            created_at: now,
            updated_at: now,
        };

        // Validate
        if let Err(e) = validate_plan(&plan) {
            errors.push(ImportError {
                plan_id: plan_id.clone(),
                error: format!("Validation failed: {}", e),
            });
            continue;
        }

        // Optional Stripe validation
        if body.validate_stripe {
            if let Err(e) = validate_plan_with_stripe(&plan, &state.stripe_client).await {
                errors.push(ImportError {
                    plan_id: plan_id.clone(),
                    error: format!("Stripe validation failed: {}", e),
                });
                continue;
            }
        }

        // Check if exists
        let existing = state.billing_store.get_plan(&plan_id).await?;

        match (existing, body.on_conflict.as_str()) {
            (Some(_), "error") => {
                errors.push(ImportError {
                    plan_id: plan_id.clone(),
                    error: "Plan already exists".to_string(),
                });
            }
            (Some(_), "skip") => {
                skipped += 1;
            }
            (Some(_), "update") => {
                if let Err(e) = state.billing_store.update_plan(&plan).await {
                    errors.push(ImportError {
                        plan_id: plan_id.clone(),
                        error: format!("Update failed: {}", e),
                    });
                } else {
                    updated += 1;
                }
            }
            (None, _) => {
                if let Err(e) = state.billing_store.create_plan(&plan).await {
                    errors.push(ImportError {
                        plan_id: plan_id.clone(),
                        error: format!("Create failed: {}", e),
                    });
                } else {
                    created += 1;
                }
            }
            (Some(_), strategy) => {
                errors.push(ImportError {
                    plan_id: plan_id.clone(),
                    error: format!("Unknown conflict strategy: {}", strategy),
                });
            }
        }
    }

    tracing::info!(
        created = created,
        updated = updated,
        skipped = skipped,
        errors = errors.len(),
        "Imported billing plans"
    );

    Ok(Json(ImportPlansResponse {
        created,
        updated,
        skipped,
        errors,
    }))
}
