/**
 * F-146 — ReportingService: summary and sales endpoints.
 *
 * Uses the CTE `filtered_orders` pattern from REPORTING_ARCHITECTURE.md §7.
 * All SQL is fully parameterized — no user input in the query string.
 */

import type pg from 'pg';
import type {
  GroupBy,
  ReportingChannel,
  ReportingFilters,
} from '../domain/filters.js';
import { comparisonRange } from './filters.js';

/** Dimensions available for groupBy in the sales endpoint. */
const GROUP_BY_DIMS = [
  'day', 'week', 'month', 'hour',
  'store', 'channel', 'terminal', 'cashier', 'payment',
] as const satisfies readonly GroupBy[];

/** Order states included in gross sales metrics. */
const SALES_STATES = [
  'PAID', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'COMPLETED',
] as const;

/** Core metric row returned by both summary and the totals row of sales. */
export interface Metrics {
  orders: number;
  customers: number;
  grossSalesCents: number;
  discountsCents: number;
  taxCents: number;
  unitsSold: number;
  shippingCents: number; // always 0 until F-146 reads shipping_cents
}

/** A single row in the sales grouped response. */
export interface SalesRow {
  period: string | null; // null for ungrouped (no groupBy)
  channel: ReportingChannel | null;
  storeId: string | null;
  terminalId: string | null;
  metrics: Metrics;
}

/** Comparison period metrics. */
export interface ComparisonMetrics {
  orders: number;
  customers: number;
  grossSalesCents: number;
  discountsCents: number;
  taxCents: number;
  unitsSold: number;
}

/** Variation between current and previous period. */
export interface Comparison {
  previous: ComparisonMetrics;
  variation?: number; // percent, null when previous is zero
}

/** Summary response. */
export interface SummaryResponse {
  range: { from: string; to: string };
  filters: {
    channel: ReportingChannel;
    storeIds: string[];
    terminalIds: string[];
  };
  comparison: Comparison | null;
  dataAvailability: {
    grossSales: 'available';
    netSales: 'unavailable';
    discounts: 'available';
    tax: 'available';
    unitsSold: 'available';
    orders: 'available';
    customers: 'available';
    margin: 'unavailable';
    paymentMethod: 'unavailable';
    refunds: 'unavailable';
    shipping: 'available';
  };
  totals: Metrics;
  updatedAt: string;
  cache: { hit: false; maxAgeSeconds: number };
}

/** Sales response with grouping and pagination. */
export interface SalesResponse {
  range: { from: string; to: string };
  filters: {
    channel: ReportingChannel;
    storeIds: string[];
    terminalIds: string[];
    groupBy: GroupBy | null;
  };
  comparison: Comparison | null;
  dataAvailability: {
    grossSales: 'available';
    netSales: 'unavailable';
    discounts: 'available';
    tax: 'available';
    unitsSold: 'available';
    orders: 'available';
    customers: 'available';
    margin: 'unavailable';
    paymentMethod: 'unavailable';
    refunds: 'unavailable';
    shipping: 'available';
  };
  items: SalesRow[];
  totals: Metrics;
  pagination: {
    page: number;
    pageSize: number;
    totalRows: number;
  };
  updatedAt: string;
  cache: { hit: false; maxAgeSeconds: number };
}

/** Internal DB row from summary CTE query. */
interface SummaryRow {
  orders: number;
  customers: number;
  gross_sales_cents: string;
  discounts_cents: string;
  tax_cents: string;
  units_sold: string;
  shipping_cents: string;
}

/** Internal DB row from sales grouped query. */
interface SalesRowRaw {
  period: string | null;
  channel: string | null;
  store_id: string | null;
  terminal_id: string | null;
  orders: number;
  customers: number;
  gross_sales_cents: string;
  discounts_cents: string;
  tax_cents: string;
  units_sold: string;
  shipping_cents: string;
}

/** Internal DB row from total count query. */
interface CountRow {
  count: string;
}

/** F-149: Product ranking row (module-level, not inside class). */
interface ProductRow {
  productId: string;
  productName: string;
  sku: string | null;
  category: string | null;
  brand: string | null;
  metrics: Metrics;
}

/** F-149: Raw DB row for product rankings. */
interface ProductRowRaw {
  product_id: string | null;
  product_name: string | null;
  sku: string | null;
  category: string | null;
  brand: string | null;
  orders: number;
  customers: number;
  gross_sales_cents: string;
  discounts_cents: string;
  tax_cents: string;
  units_sold: string;
  shipping_cents: string;
}


/** F-149: Products ranking response. */
export interface ProductsResponse extends Omit<SalesResponse, 'items'> {
  items: ProductRow[];
}

export class ReportingService {
  constructor(private readonly pool: pg.Pool) {}

  /**
   * Aggregated summary metrics for the given filters.
   *
   * `dataAvailability` reflects the current data model:
   * - grossSales: available (orders + items)
   * - netSales: unavailable (needs shipping_cents per-order, available from F-144 but service doesn't read it yet)
   * - discounts: available (orders_items.discount_cents)
   * - tax: available (orders_items.tax_cents)
   * - unitsSold: available (SUM quantity)
   * - orders/customers: available
   * - margin: unavailable (cost_at_sale not populated)
   * - paymentMethod: unavailable (needs JOIN with reporting_payment_lines)
   * - refunds: unavailable (needs state filter)
   * - shipping: available (orders_orders.shipping_cents from F-144)
   */
  async summary(filters: ReportingFilters): Promise<SummaryResponse> {
    const { range, channel, storeIds, terminalIds } = filters;

    const [current, totalRows] = await Promise.all([
      this.runSummaryQuery(range.from, range.to, channel, storeIds, terminalIds),
      this.runCountQuery(range.from, range.to, channel, storeIds, terminalIds),
    ]);

    const comparison =
      filters.compare === 'none'
        ? null
        : {
            previous: current, // same row — reuse current query; comparison range is metadata only here
            variation: undefined as number | undefined,
          };

    return {
      range,
      filters: { channel, storeIds, terminalIds },
      comparison,
      dataAvailability: {
        grossSales: 'available',
        netSales: 'unavailable',
        discounts: 'available',
        tax: 'available',
        unitsSold: 'available',
        orders: 'available',
        customers: 'available',
        margin: 'unavailable',
        paymentMethod: 'unavailable',
        refunds: 'unavailable',
        shipping: 'available',
      },
      totals: current,
      updatedAt: new Date().toISOString(),
      cache: { hit: false, maxAgeSeconds: 0 },
    };
  }

  /**
   * Grouped sales rows with pagination.
   *
   * When `groupBy` is set, rows are grouped by that dimension.
   * When `groupBy` is absent, returns a single ungrouped row (same as summary).
   * Pagination applies to the grouped rows (total count for LIMIT/OFFSET).
   */
  async sales(filters: ReportingFilters): Promise<SalesResponse> {
    const { range, channel, storeIds, terminalIds, groupBy, page, pageSize } = filters;
    const offset = (page - 1) * pageSize;

    const [rows, totalRows] = await Promise.all([
      this.runSalesQuery(range.from, range.to, channel, storeIds, terminalIds, groupBy, pageSize, offset),
      this.runCountQuery(range.from, range.to, channel, storeIds, terminalIds),
    ]);

    const totals = await this.runSummaryQuery(range.from, range.to, channel, storeIds, terminalIds);

    const comparison =
      filters.compare === 'none'
        ? null
        : { previous: totals, variation: undefined as number | undefined };

    return {
      range,
      filters: { channel, storeIds, terminalIds, groupBy: groupBy ?? null },
      comparison,
      dataAvailability: {
        grossSales: 'available',
        netSales: 'unavailable',
        discounts: 'available',
        tax: 'available',
        unitsSold: 'available',
        orders: 'available',
        customers: 'available',
        margin: 'unavailable',
        paymentMethod: 'unavailable',
        refunds: 'unavailable',
        shipping: 'available',
      },
      items: rows,
      totals,
      pagination: { page, pageSize, totalRows },
      updatedAt: new Date().toISOString(),
      cache: { hit: false, maxAgeSeconds: 0 },
    };
  }

  // ── Private query helpers ────────────────────────────────────────────────

  private async runSummaryQuery(
    from: string,
    to: string,
    channel: ReportingChannel,
    storeIds: string[],
    terminalIds: string[],
  ): Promise<Metrics> {
    const channelFilter = channel === 'all' ? null : channel;

    const result = await this.pool.query<SummaryRow>(
      `WITH filtered_orders AS (
        SELECT o.id, o.user_id, o.state, o.source, o.store_id, o.terminal_id, o.created_at, o.shipping_cents
        FROM orders_orders o
        WHERE o.created_at >= $1
          AND o.created_at < $2
          AND o.state = ANY($3::text[])
          AND ($4::text IS NULL OR o.source = $4)
          AND (cardinality($5::uuid[]) = 0 OR o.store_id = ANY($5::uuid[]))
          AND (cardinality($6::uuid[]) = 0 OR o.terminal_id = ANY($6::uuid[]))
      ),
      filtered_items AS (
        SELECT i.order_id, i.quantity, i.unit_price_cents, i.discount_cents, i.tax_cents
        FROM orders_items i
        WHERE i.order_id IN (SELECT id FROM filtered_orders)
      )
      SELECT
        COUNT(DISTINCT o.id)::int            AS orders,
        COUNT(DISTINCT o.user_id) FILTER (WHERE o.user_id IS NOT NULL)::int AS customers,
        COALESCE(SUM(i.unit_price_cents * i.quantity), 0)::bigint AS gross_sales_cents,
        COALESCE(SUM(i.discount_cents), 0)::bigint               AS discounts_cents,
        COALESCE(SUM(i.tax_cents), 0)::bigint                   AS tax_cents,
        COALESCE(SUM(i.quantity), 0)::int                        AS units_sold,
        COALESCE(SUM(o.shipping_cents), 0)::bigint               AS shipping_cents
      FROM filtered_orders o
      LEFT JOIN filtered_items i ON i.order_id = o.id`,
      [from, to, [...SALES_STATES], channelFilter, storeIds, terminalIds],
    );
    const row = result.rows[0] ?? {
      orders: 0, customers: 0, gross_sales_cents: '0',
      discounts_cents: '0', tax_cents: '0', units_sold: '0', shipping_cents: '0',
    };
    return {
      orders: Number(row.orders) || 0,
      customers: Number(row.customers) || 0,
      grossSalesCents: Number(row.gross_sales_cents) || 0,
      discountsCents: Number(row.discounts_cents) || 0,
      taxCents: Number(row.tax_cents) || 0,
      unitsSold: Number(row.units_sold) || 0,
      shippingCents: Number(row.shipping_cents) || 0,
    };
  }

  private async runSalesQuery(
    from: string,
    to: string,
    channel: ReportingChannel,
    storeIds: string[],
    terminalIds: string[],
    groupBy: GroupBy | undefined,
    pageSize: number,
    offset: number,
  ): Promise<SalesRow[]> {
    const channelFilter = channel === 'all' ? null : channel;

    // Build group-by clause
    const { groupExpr, selectExpr } = this.buildGroupBy(groupBy);

    const query = `
      WITH filtered_orders AS (
        SELECT o.id, o.user_id, o.state, o.source, o.store_id, o.terminal_id, o.created_at, o.shipping_cents
        FROM orders_orders o
        WHERE o.created_at >= $1
          AND o.created_at < $2
          AND o.state = ANY($3::text[])
          AND ($4::text IS NULL OR o.source = $4)
          AND (cardinality($5::uuid[]) = 0 OR o.store_id = ANY($5::uuid[]))
          AND (cardinality($6::uuid[]) = 0 OR o.terminal_id = ANY($6::uuid[]))
      ),
      filtered_items AS (
        SELECT i.order_id, i.quantity, i.unit_price_cents, i.discount_cents, i.tax_cents
        FROM orders_items i
        WHERE i.order_id IN (SELECT id FROM filtered_orders)
      )
      SELECT
        ${selectExpr},
        COUNT(DISTINCT o.id)::int            AS orders,
        COUNT(DISTINCT o.user_id) FILTER (WHERE o.user_id IS NOT NULL)::int AS customers,
        COALESCE(SUM(i.unit_price_cents * i.quantity), 0)::bigint AS gross_sales_cents,
        COALESCE(SUM(i.discount_cents), 0)::bigint               AS discounts_cents,
        COALESCE(SUM(i.tax_cents), 0)::bigint                   AS tax_cents,
        COALESCE(SUM(i.quantity), 0)::int                        AS units_sold,
        COALESCE(SUM(o.shipping_cents), 0)::bigint               AS shipping_cents
      FROM filtered_orders o
      LEFT JOIN filtered_items i ON i.order_id = o.id
      GROUP BY ${groupExpr}
      ORDER BY ${(groupExpr.split(',')[0] ?? '1').trim()}${groupBy ? `, ${groupExpr}` : ''}
      LIMIT $7 OFFSET $8`;

    const result = await this.pool.query<SalesRowRaw>(query, [
      from, to, [...SALES_STATES], channelFilter, storeIds, terminalIds, pageSize, offset,
    ]);
    return result.rows.map(this.toSalesRow);
  }

  private async runCountQuery(
    from: string,
    to: string,
    channel: ReportingChannel,
    storeIds: string[],
    terminalIds: string[],
  ): Promise<number> {
    const channelFilter = channel === 'all' ? null : channel;
    const result = await this.pool.query<CountRow>(
      `SELECT COUNT(DISTINCT o.id)::text AS count
       FROM orders_orders o
       WHERE o.created_at >= $1
         AND o.created_at < $2
         AND o.state = ANY($3::text[])
         AND ($4::text IS NULL OR o.source = $4)
         AND (cardinality($5::uuid[]) = 0 OR o.store_id = ANY($5::uuid[]))
         AND (cardinality($6::uuid[]) = 0 OR o.terminal_id = ANY($6::uuid[]))`,
      [from, to, [...SALES_STATES], channelFilter, storeIds, terminalIds],
    );
    return Number(result.rows[0]?.count ?? 0);
  }

  // ── F-149: Product rankings ───────────────────────────────────────────

  /**
   * Top N products by units sold or revenue within the filter range.
   * Joins orders_items with catalog_products/categories/brands.
   */
  async products(filters: ReportingFilters): Promise<ProductsResponse> {
    const { range, channel, storeIds, terminalIds } = filters;
    const channelFilter = channel === 'all' ? null : channel;
    const sortBy: string = ((filters as { sort?: string }).sort ?? '') === 'revenue'
      ? 'gross_sales_cents'
      : 'units_sold';
    const pageSize = filters.pageSize ?? 20;
    const offset = ((filters.page ?? 1) - 1) * pageSize;

    const result = await this.pool.query<
      ProductRowRaw & { total_count: string }
    >(
      `WITH filtered_orders AS (
        SELECT o.id, o.user_id, o.state, o.source, o.store_id, o.terminal_id, o.shipping_cents
        FROM orders_orders o
        WHERE o.created_at >= $1
          AND o.created_at < $2
          AND o.state = ANY($3::text[])
          AND ($4::text IS NULL OR o.source = $4)
          AND (cardinality($5::uuid[]) = 0 OR o.store_id = ANY($5::uuid[]))
          AND (cardinality($6::uuid[]) = 0 OR o.terminal_id = ANY($6::uuid[]))
      ),
      filtered_items AS (
        SELECT i.order_id, i.product_id, i.sku, i.quantity, i.unit_price_cents, i.discount_cents, i.tax_cents
        FROM orders_items i
        WHERE i.order_id IN (SELECT id FROM filtered_orders)
      ),
      product_rank AS (
        SELECT
          p.id              AS product_id,
          p.name            AS product_name,
          MIN(fi.sku)       AS sku,
          c.name            AS category,
          b.name            AS brand,
          COUNT(DISTINCT fi.order_id)::int                                AS orders,
          COUNT(DISTINCT o.user_id) FILTER (WHERE o.user_id IS NOT NULL)::int AS customers,
          COALESCE(SUM(fi.unit_price_cents * fi.quantity), 0)::bigint    AS gross_sales_cents,
          COALESCE(SUM(fi.discount_cents), 0)::bigint                    AS discounts_cents,
          COALESCE(SUM(fi.tax_cents), 0)::bigint                        AS tax_cents,
          COALESCE(SUM(fi.quantity), 0)::int                             AS units_sold,
          COALESCE(SUM(o.shipping_cents), 0)::bigint                    AS shipping_cents
        FROM filtered_items fi
        JOIN filtered_orders o ON o.id = fi.order_id
        JOIN catalog_products p ON p.id = fi.product_id
        LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
        LEFT JOIN categories_categories c ON c.id = pc.category_id
        LEFT JOIN brands_brands b ON b.id = p.brand_id
        GROUP BY p.id, p.name, c.name, b.name
        ORDER BY ${sortBy} DESC
        LIMIT $7 OFFSET $8
      ),
      total_count AS (SELECT COUNT(*)::int AS count FROM product_rank)
      SELECT pr.*, tc.count AS total_count
      FROM product_rank pr
      CROSS JOIN total_count tc`,
      [
        range.from, range.to, [...SALES_STATES],
        channelFilter, storeIds, terminalIds,
        pageSize, offset,
      ],
    );

    const rows: ProductRow[] = result.rows.map((r) => ({
      productId: r.product_id ?? '',
      productName: r.product_name ?? 'Sin nombre',
      sku: r.sku ?? null,
      category: r.category ?? null,
      brand: r.brand ?? null,
      metrics: {
        orders: r.orders ?? 0,
        customers: r.customers ?? 0,
        grossSalesCents: Number(r.gross_sales_cents) || 0,
        discountsCents: Number(r.discounts_cents) || 0,
        taxCents: Number(r.tax_cents) || 0,
        unitsSold: Number(r.units_sold) || 0,
        shippingCents: Number(r.shipping_cents) || 0,
      },
    }));
    const totalRows = Number(result.rows[0]?.total_count ?? 0);

    const totals = await this.runSummaryQuery(
      range.from, range.to, channel, storeIds, terminalIds,
    );

    return {
      range,
      filters: { channel, storeIds, terminalIds, groupBy: null },
      comparison: null,
      dataAvailability: {
        grossSales: 'available',
        netSales: 'unavailable',
        discounts: 'available',
        tax: 'available',
        unitsSold: 'available',
        orders: 'available',
        customers: 'available',
        margin: 'unavailable',
        paymentMethod: 'unavailable',
        refunds: 'unavailable',
        shipping: 'available',
      },
      items: rows,
      totals,
      pagination: { page: filters.page ?? 1, pageSize, totalRows },
      updatedAt: new Date().toISOString(),
      cache: { hit: false, maxAgeSeconds: 0 },
    };
  }


    private buildGroupBy(dim: GroupBy | undefined): {
      groupExpr: string;
      selectExpr: string;
    } {
      if (!dim) {
        return {
          groupExpr: '1', // single group
          selectExpr: 'NULL::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id',
        };
      }
      switch (dim) {
        case 'day':
          return {
            groupExpr: 'period',
            selectExpr: "DATE_TRUNC('day', o.created_at)::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id",
          };
        case 'week':
          return {
            groupExpr: 'period',
            selectExpr: "DATE_TRUNC('week', o.created_at)::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id",
          };
        case 'month':
          return {
            groupExpr: 'period',
            selectExpr: "DATE_TRUNC('month', o.created_at)::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id",
          };
        case 'hour':
          return {
            groupExpr: 'period',
            selectExpr: "DATE_TRUNC('hour', o.created_at)::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id",
          };
        case 'store':
          return {
            groupExpr: 'store_id',
            selectExpr: "NULL::text AS period, NULL::text AS channel, o.store_id, NULL::uuid AS terminal_id",
          };
        case 'channel':
          return {
            groupExpr: 'channel',
            selectExpr: "NULL::text AS period, o.source AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id",
          };
        case 'terminal':
          return {
            groupExpr: 'store_id, terminal_id',
            selectExpr: "NULL::text AS period, NULL::text AS channel, o.store_id, o.terminal_id",
          };
        default:
          // cashier / payment: not yet joined — group by 1 as fallback
          return {
            groupExpr: '1',
            selectExpr: 'NULL::text AS period, NULL::text AS channel, NULL::uuid AS store_id, NULL::uuid AS terminal_id',
          };
      }
    }
  
    private toSalesRow(r: SalesRowRaw): SalesRow {
      return {
        period: r.period,
        channel: (r.channel ?? null) as ReportingChannel | null,
        storeId: r.store_id ?? null,
        terminalId: r.terminal_id ?? null,
        metrics: {
          orders: r.orders ?? 0,
          customers: r.customers ?? 0,
          grossSalesCents: Number(r.gross_sales_cents) || 0,
          discountsCents: Number(r.discounts_cents) || 0,
          taxCents: Number(r.tax_cents) || 0,
          unitsSold: Number(r.units_sold) || 0,
          shippingCents: Number(r.shipping_cents) || 0,
        },
      };
  }
}
