import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MealieMapper } from './mealie.mapper.js';
import type { IMealieConfig, IMealieRawData, IMealieSnapshot } from './mealie.types.js';
import { mealieProfile } from './mealie.types.js';

export class MealieApiError extends Error {}
export class MealieApiConnectionError extends MealieApiError {}
export class MealieApiAuthorizationError extends MealieApiError {}
export class MealieApiValidationError extends MealieApiError {}
export class MealieApiNotFoundError extends MealieApiError {}
export class MealieUnsupportedCommandError extends MealieApiError {}

interface IMealieEndpoint {
  baseUrl: string;
  host: string;
  port?: number;
  protocol: 'http' | 'https';
}

interface IMealieHttpResponse {
  status: number;
  text: string;
  contentType: string;
}

export class MealieClient extends SimpleLocalClient<IMealieConfig> {
  private currentMealieSnapshot?: IMealieSnapshot;

  constructor(private readonly configArg: IMealieConfig) {
    super(mealieProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IMealieSnapshot> {
    if (!forceRefreshArg && this.currentMealieSnapshot) {
      return clone(this.currentMealieSnapshot);
    }

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentMealieSnapshot = MealieMapper.toSnapshot({ config: this.configArg, source: 'snapshot', online: this.configArg.snapshot.online });
      return clone(this.currentMealieSnapshot);
    }

    if (this.configArg.client) {
      try {
        this.currentMealieSnapshot = await this.snapshotFromMealieClient();
      } catch (errorArg) {
        this.currentMealieSnapshot = this.offlineMealieSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentMealieSnapshot);
    }

    if (this.configArg.snapshotProvider) {
      try {
        const provided = await this.configArg.snapshotProvider();
        if (provided !== undefined) {
          this.currentMealieSnapshot = MealieMapper.toSnapshot({ config: this.configArg, rawData: provided, source: 'provider', online: true });
          return clone(this.currentMealieSnapshot);
        }
      } catch (errorArg) {
        this.currentMealieSnapshot = this.offlineMealieSnapshot(errorMessage(errorArg));
        return clone(this.currentMealieSnapshot);
      }
    }

    if (!forceRefreshArg && this.hasManualData()) {
      const rawData = this.configArg.rawData ?? this.configArg.rawText;
      this.currentMealieSnapshot = MealieMapper.toSnapshot({ config: this.configArg, rawData, online: this.configArg.online ?? true, source: 'manual' });
      return clone(this.currentMealieSnapshot);
    }

    if (this.endpoint()) {
      try {
        this.currentMealieSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentMealieSnapshot = this.offlineMealieSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentMealieSnapshot);
    }

    this.currentMealieSnapshot = this.offlineMealieSnapshot('Mealie snapshots require config.host/config.url plus token/apiToken, an injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentMealieSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentMealieSnapshot = undefined;
    const liveCapable = Boolean(this.configArg.client || this.configArg.snapshotProvider || this.endpoint());
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'Mealie refresh requires a live HTTP endpoint, injected client, or snapshotProvider.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<IMealieSnapshot> {
    const endpoint = this.requireEndpoint();
    if (!this.accessToken()) {
      throw new MealieApiAuthorizationError('Mealie local API snapshots require config.token or config.apiToken.');
    }
    if (endpoint.baseUrl.includes('/app/')) {
      throw new MealieApiConnectionError('Mealie ingress /app/ URLs are not standalone local API endpoints. Configure the direct Mealie base URL.');
    }
    const rawData = await this.fetchRawData(endpoint);
    return MealieMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    try {
      if (this.configArg.commandExecutor) {
        return normalizeServiceResult(await this.configArg.commandExecutor.execute(requestArg));
      }
      if (this.configArg.client?.execute) {
        return normalizeServiceResult(await this.configArg.client.execute(requestArg));
      }
      if (requestArg.service === 'refresh') {
        return this.refresh();
      }
      if (!this.endpoint()) {
        throw new MealieApiConnectionError('Mealie commands require config.host/config.url with token/apiToken, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
      }
      if (!this.accessToken()) {
        throw new MealieApiAuthorizationError('Mealie local API commands require config.token or config.apiToken.');
      }

      const endpoint = this.requireEndpoint();
      const data = requestArg.data || {};
      if (requestArg.service === 'get_mealplan') {
        const mealplans = await this.getMealplans(endpoint, dateValue(data.start_date ?? data.startDate) || today(), dateValue(data.end_date ?? data.endDate) || today());
        return { success: true, data: { mealplan: arrayValue(mealplans.items) || [] } };
      }
      if (requestArg.service === 'get_recipe') {
        const recipeId = requiredString(data.recipe_id ?? data.recipeId, 'get_recipe requires recipe_id.');
        return { success: true, data: { recipe: await this.requestJson(endpoint, `api/recipes/${encodeURIComponent(recipeId)}`) } };
      }
      if (requestArg.service === 'get_recipes') {
        const params: Record<string, string | number> = { perPage: numberValue(data.result_limit ?? data.resultLimit) || 10 };
        const search = stringValue(data.search_terms ?? data.searchTerms);
        if (search) {
          params.search = search;
        }
        return { success: true, data: { recipes: await this.requestJson(endpoint, 'api/recipes', { params }) } };
      }
      if (requestArg.service === 'get_shopping_list_items') {
        const listId = requiredString(data.shopping_list_id ?? data.shoppingListId ?? data.list_id ?? data.listId, 'get_shopping_list_items requires shopping_list_id.');
        const items = await this.getShoppingItems(endpoint, listId);
        return { success: true, data: { shopping_list_id: listId, items: arrayValue(items.items) || [] } };
      }
      if (requestArg.service === 'import_recipe') {
        const url = requiredString(data.url, 'import_recipe requires url.');
        const recipeId = await this.requestJson<string>(endpoint, 'api/recipes/create/url', { method: 'POST', body: { url, include_tags: Boolean(data.include_tags ?? data.includeTags) } });
        const recipe = await this.requestJson(endpoint, `api/recipes/${encodeURIComponent(String(recipeId))}`);
        this.currentMealieSnapshot = undefined;
        return { success: true, data: { recipe } };
      }
      if (requestArg.service === 'set_random_mealplan') {
        const mealplan = await this.requestJson(endpoint, 'api/households/mealplans/random', { method: 'POST', body: { date: requiredString(data.date, 'set_random_mealplan requires date.'), entryType: requiredString(data.entry_type ?? data.entryType, 'set_random_mealplan requires entry_type.') } });
        this.currentMealieSnapshot = undefined;
        return { success: true, data: { mealplan } };
      }
      if (requestArg.service === 'set_mealplan') {
        const body: Record<string, unknown> = {
          date: requiredString(data.date, 'set_mealplan requires date.'),
          entryType: requiredString(data.entry_type ?? data.entryType, 'set_mealplan requires entry_type.'),
        };
        const recipeId = stringValue(data.recipe_id ?? data.recipeId);
        if (recipeId) {
          body.recipeId = recipeId;
        } else {
          body.title = requiredString(data.note_title ?? data.noteTitle, 'set_mealplan requires recipe_id or note_title.');
          const noteText = stringValue(data.note_text ?? data.noteText);
          if (noteText) {
            body.text = noteText;
          }
        }
        const mealplan = await this.requestJson(endpoint, 'api/households/mealplans', { method: 'POST', body });
        this.currentMealieSnapshot = undefined;
        return { success: true, data: { mealplan } };
      }
      return { success: false, error: `Unsupported Mealie service: ${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.configArg.client?.destroy?.();
  }

  private async snapshotFromMealieClient(): Promise<IMealieSnapshot> {
    const client = this.configArg.client;
    if (!client) {
      throw new MealieApiConnectionError('No Mealie client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (isSimpleSnapshot(result)) {
        return MealieMapper.toSnapshot({ config: { ...this.configArg, snapshot: result }, source: 'client', online: result.online });
      }
      return MealieMapper.toSnapshot({ config: this.configArg, rawData: result, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return MealieMapper.toSnapshot({ config: this.configArg, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    if (client.getState) {
      return MealieMapper.toSnapshot({ config: this.configArg, rawData: await client.getState(), online: true, source: 'client' });
    }
    throw new MealieApiConnectionError('Mealie client must expose getSnapshot(), getRawData(), or getState().');
  }

  private async fetchRawData(endpointArg: IMealieEndpoint): Promise<IMealieRawData> {
    const rawData: IMealieRawData = { fetchedAt: new Date().toISOString(), errors: {} };
    rawData.about = await this.requestJson(endpointArg, 'api/app/about');
    rawData.user = await this.requestJson(endpointArg, 'api/users/self');
    rawData.statistics = await this.requestJson(endpointArg, 'api/households/statistics');
    rawData.mealplans = await this.optionalResource(endpointArg, 'api/households/mealplans', { start_date: today(), end_date: addDays(7), perPage: -1 }, 'mealplans');
    rawData.shoppingLists = await this.optionalResource(endpointArg, 'api/households/shopping/lists', { perPage: -1 }, 'shoppingLists');

    const shoppingLists = arrayValue(rawData.shoppingLists?.items);
    if (shoppingLists?.length) {
      rawData.shoppingItems = {};
      for (const list of shoppingLists) {
        const listId = stringValue(recordValue(list)?.id);
        if (listId) {
          rawData.shoppingItems[listId] = await this.optionalResource(endpointArg, 'api/households/shopping/items', { queryFilter: `shoppingListId=${listId}`, orderBy: 'position', orderDirection: 'asc', perPage: -1 }, `shoppingItems.${listId}`) || { items: [] };
        }
      }
    }

    if (rawData.errors && !Object.keys(rawData.errors).length) {
      delete rawData.errors;
    }
    return rawData;
  }

  private async optionalResource(endpointArg: IMealieEndpoint, pathArg: string, paramsArg: Record<string, string | number>, errorKeyArg: string): Promise<Record<string, unknown> | undefined> {
    try {
      return await this.requestJson(endpointArg, pathArg, { params: paramsArg });
    } catch (errorArg) {
      return { items: [], error: errorMessage(errorArg), errorKey: errorKeyArg };
    }
  }

  private async getMealplans(endpointArg: IMealieEndpoint, startDateArg: string, endDateArg: string): Promise<Record<string, unknown>> {
    return this.requestJson(endpointArg, 'api/households/mealplans', { params: { start_date: startDateArg, end_date: endDateArg, perPage: -1 } });
  }

  private async getShoppingItems(endpointArg: IMealieEndpoint, listIdArg: string): Promise<Record<string, unknown>> {
    return this.requestJson(endpointArg, 'api/households/shopping/items', { params: { queryFilter: `shoppingListId=${listIdArg}`, orderBy: 'position', orderDirection: 'asc', perPage: -1 } });
  }

  private async requestJson<TValue = Record<string, unknown>>(endpointArg: IMealieEndpoint, pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: unknown; params?: Record<string, string | number | boolean> } = {}): Promise<TValue> {
    const response = await this.requestText(endpointArg, pathArg, optionsArg);
    if (!response.text) {
      return {} as TValue;
    }
    return JSON.parse(response.text) as TValue;
  }

  private async requestText(endpointArg: IMealieEndpoint, pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: unknown; params?: Record<string, string | number | boolean> } = {}): Promise<IMealieHttpResponse> {
    const url = new URL(`${endpointArg.baseUrl}/${pathArg.replace(/^\/+/, '')}`);
    for (const [key, value] of Object.entries(optionsArg.params || {})) {
      url.searchParams.set(key, String(value));
    }
    const timeoutMs = this.configArg.timeoutMs || 10000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const headers: Record<string, string> = {
      Accept: 'application/json, text/plain, */*',
      'User-Agent': 'SmarthomeExchange-Mealie/1.0',
    };
    const token = this.accessToken();
    if (token) {
      headers.Authorization = `Bearer ${token}`;
    }
    if (optionsArg.body !== undefined) {
      headers['Content-Type'] = 'application/json';
    }

    try {
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
        signal: controller.signal,
      });
      const text = await response.text();
      const contentType = response.headers.get('content-type') || '';
      if (response.status === 400) {
        throw new MealieApiValidationError('Bad request to Mealie.');
      }
      if (response.status === 401) {
        throw new MealieApiAuthorizationError('Unauthorized access to Mealie.');
      }
      if (response.status === 404) {
        throw new MealieApiNotFoundError('Object not found in Mealie.');
      }
      if (response.status === 422) {
        throw new MealieApiValidationError('Mealie validation error.');
      }
      if (!response.ok) {
        throw new MealieApiError(`Mealie API request failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
      }
      if (text && !contentType.includes('application/json')) {
        throw new MealieApiError(`Unexpected response from Mealie: ${contentType || 'missing content-type'}`);
      }
      return { status: response.status, text, contentType };
    } catch (errorArg) {
      if (errorArg instanceof MealieApiError) {
        throw errorArg;
      }
      throw new MealieApiConnectionError(errorMessage(errorArg));
    } finally {
      clearTimeout(timer);
    }
  }

  private requireEndpoint(): IMealieEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new MealieApiConnectionError('Mealie local API requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IMealieEndpoint | undefined {
    const value = this.configArg.url || this.configArg.host;
    if (!value) {
      return undefined;
    }
    const protocol = this.configArg.useTls ? 'https' : 'http';
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${protocol}://${value}`);
      if (this.configArg.port) {
        url.port = String(this.configArg.port);
      }
      return {
        baseUrl: url.toString().replace(/\/$/, ''),
        host: url.hostname,
        port: this.configArg.port || (url.port ? Number(url.port) : undefined),
        protocol: url.protocol === 'https:' ? 'https' : 'http',
      };
    } catch {
      return undefined;
    }
  }

  private accessToken(): string | undefined {
    return stringValue(this.configArg.token) || stringValue(this.configArg.apiToken);
  }

  private hasManualData(): boolean {
    return Boolean(this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length || this.configArg.snapshot);
  }

  private offlineMealieSnapshot(errorArg: string): IMealieSnapshot {
    return MealieMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData, online: false, source: 'runtime', error: errorArg });
  }
}

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const isSimpleSnapshot = (valueArg: unknown): valueArg is ISimpleLocalSnapshot => Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;

const arrayValue = (valueArg: unknown): unknown[] | undefined => Array.isArray(valueArg) ? valueArg : undefined;

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Math.round(Number(valueArg));
  }
  return undefined;
};

const dateValue = (valueArg: unknown): string | undefined => {
  if (valueArg instanceof Date && Number.isFinite(valueArg.getTime())) {
    return valueArg.toISOString().slice(0, 10);
  }
  return stringValue(valueArg);
};

const today = (): string => new Date().toISOString().slice(0, 10);

const addDays = (daysArg: number): string => {
  const date = new Date();
  date.setUTCDate(date.getUTCDate() + daysArg);
  return date.toISOString().slice(0, 10);
};

const requiredString = (valueArg: unknown, messageArg: string): string => {
  const value = dateValue(valueArg) || stringValue(valueArg);
  if (!value) {
    throw new MealieUnsupportedCommandError(messageArg);
  }
  return value;
};

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
