import { z } from "zod";
import { contentJson } from "../contentTypes";
import { InputValidationException } from "../exceptions";
import { OpenAPIRoute } from "../route";
import type { AnyZodObject, OrderByDirection } from "../types";
import {
  type FilterCondition,
  type ListFilters,
  type ListResult,
  MetaGenerator,
  type MetaInput,
  metaSchemaProps,
  type O,
} from "./types";

export class ListEndpoint<HandleArgs extends Array<object> = Array<object>> extends OpenAPIRoute<HandleArgs> {
  // @ts-expect-error
  _meta: MetaInput;

  get meta() {
    return MetaGenerator(this._meta);
  }

  filterFields?: Array<string>;
  searchFields?: Array<string>;
  searchFieldName = "search";
  pageFieldName = "page";
  perPageFieldName = "per_page";
  orderByFieldName = "order_by";
  orderByDirectionFieldName = "order_by_direction";
  // Explicitly type orderByFields to avoid narrow never[] inference for subclasses
  orderByFields: string[] = [];
  defaultOrderBy?: string;
  /** Default sort direction when order_by is used. Defaults to "asc". */
  defaultOrderByDirection: OrderByDirection = "asc";

  get optionFields(): string[] {
    return [this.pageFieldName, this.perPageFieldName, this.orderByFieldName, this.orderByDirectionFieldName];
  }

  getSchema() {
    const parsedQueryParameters = this.meta.fields.pick(
      (this.filterFields || [])
        .filter((item) => !new Set(this.params.urlParams || []).has(item))
        .reduce((a, v) => ({ ...a, [v]: true }), {}),
    ).shape;
    const pathParameters = this.meta.fields.pick(
      (this.params.urlParams || this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {}),
    );

    for (const [key, value] of Object.entries(parsedQueryParameters)) {
      // @ts-expect-error  TODO: check this
      parsedQueryParameters[key] = (value as AnyZodObject).optional();
    }

    if (this.searchFields) {
      // @ts-expect-error  TODO: check this
      parsedQueryParameters[this.searchFieldName] = z
        .string()
        .optional()
        .openapi({
          description: `Search by ${this.searchFields.join(", ")}`,
        });
    }

    let queryParameters = z
      .object({
        [this.pageFieldName]: z.number().int().min(1).optional().default(1),
        [this.perPageFieldName]: z.number().int().min(1).max(100).optional().default(20),
      })
      .extend(parsedQueryParameters);

    if (this.orderByFields && this.orderByFields.length > 0) {
      const orderByFieldsTuple = this.orderByFields as [string, ...string[]];
      queryParameters = queryParameters.extend({
        [this.orderByFieldName]: z
          .enum(orderByFieldsTuple)
          .optional()
          .default(orderByFieldsTuple[0])
          .describe("Order By Column Name"),
        [this.orderByDirectionFieldName]: z
          .enum(["asc", "desc"])
          .optional()
          .default(this.defaultOrderByDirection)
          .describe("Order By Direction"),
      } as any); // Cast needed: computed property keys widen the type beyond what Zod's .extend() accepts
    }

    return {
      request: {
        params: Object.keys(pathParameters.shape).length ? pathParameters : undefined,
        query: queryParameters,
        ...this.schema?.request,
      },
      responses: {
        "200": {
          description: "List objects",
          ...contentJson(
            z.object({
              success: z.boolean(),
              result: z.array(this.meta.model.serializerSchema),
            }),
          ),
          ...this.schema?.responses?.[200],
        },
        ...InputValidationException.schema(),
        ...this.schema?.responses,
      },
      ...metaSchemaProps(this._meta),
      ...this.schema,
    };
  }

  async getFilters(): Promise<ListFilters> {
    const data = await this.getValidatedData();

    const filters: Array<FilterCondition> = [];
    const options: Record<string, string> = {}; // TODO: fix this type

    for (const part of [data.params, data.query]) {
      if (part) {
        for (const [key, value] of Object.entries(part)) {
          if (this.searchFields && key === this.searchFieldName) {
            filters.push({
              field: key,
              operator: "LIKE",
              value: value as string,
            });
          } else if (this.optionFields.includes(key)) {
            options[key] = value as string;
          } else {
            filters.push({
              field: key,
              operator: "EQ",
              value: value as string,
            });
          }
        }
      }
    }

    return {
      options,
      filters,
    };
  }

  async before(filters: ListFilters): Promise<ListFilters> {
    return filters;
  }

  async after(data: ListResult<O<typeof this._meta>>): Promise<ListResult<O<typeof this._meta>>> {
    return data;
  }

  async list(_filters: ListFilters): Promise<ListResult<O<typeof this._meta>>> {
    return {
      result: [],
    };
  }

  async handle(..._args: HandleArgs) {
    let filters = await this.getFilters();

    filters = await this.before(filters);

    let objs = await this.list(filters);

    objs = await this.after(objs);

    objs = {
      ...objs,
      result: objs.result.map((item) =>
        this.meta.model.serializer(item, { filters: filters.filters, options: filters.options }),
      ),
    };

    return {
      success: true,
      ...objs,
    };
  }
}
