/**
 * AdkTransport - Simplified transport for import operations
 *
 * Focused on the import use case:
 * - Get transport by number
 * - Iterate objects
 * - Load each object as proper ADK type
 *
 * Architecture:
 * - Uses ADT contracts via ctx.client.adt.cts.transportrequests.get()
 * - No business logic beyond object iteration
 * - Object loading delegated to ADK registry
 */

import type { AdkContext } from '../../base/context';
import { getGlobalContext } from '../../base/global-context';
import type { TransportGetResponse } from '../../base/adt';
import type { TransportData } from './transport/transport.types';

// Types from the transport schema
// These match the structure in transportmanagment-single.types.ts
interface TransportObjectData {
  pgmid?: string;
  type?: string;
  name?: string;
  wbtype?: string;
  uri?: string;
  obj_desc?: string;
  obj_info?: string;
  lock_status?: string;
  /** Object function code (e.g. 'D' = deletion, 'K' = key, '' = modification) */
  obj_func?: string;
}

/**
 * Selector for filtering transport objects.
 *
 * All specified dimensions are ANDed together.
 * Use '*' for a wildcard that matches any non-empty value.
 */
export interface TransportObjectSelector {
  /**
   * Filter by object function code.
   * 'D' = deletion only, 'K' = key only, '*' = any non-empty value, '' = empty (normal)
   */
  objFunc?: string | string[];
  /**
   * Filter by program ID.
   * 'R3TR' = repository objects, 'LIMU' = sub-objects, '*' = any
   */
  pgmid?: string | string[];
  /**
   * Filter by object type (e.g. 'CLAS', 'TABL', '*' = any)
   */
  type?: string | string[];
}

interface TransportTaskData {
  number?: string;
  owner?: string;
  desc?: string;
  status?: string;
  status_text?: string;
  target?: string;
  abap_object?: TransportObjectData | TransportObjectData[];
}

// Re-export the response type for consumers
export type { TransportGetResponse as TransportResponse };

/**
 * Helper to normalize array/single value to array
 */
function asArray<T>(val: T | T[] | undefined): T[] {
  if (!val) return [];
  return Array.isArray(val) ? val : [val];
}

/**
 * Check if a value matches a selector dimension.
 *
 * - If `expected` is undefined → no filter (always matches).
 * - If `expected` is '*' → matches any truthy value.
 * - If `expected` is an array → value must be in the array.
 * - Otherwise exact match (case-insensitive).
 */
function matchesDimension(
  value: string,
  expected: string | string[] | undefined,
): boolean {
  if (expected === undefined) return true;
  if (Array.isArray(expected)) {
    return expected.some((e) =>
      e === '*' ? !!value : e.toUpperCase() === value.toUpperCase(),
    );
  }
  if (expected === '*') return !!value;
  return expected.toUpperCase() === value.toUpperCase();
}

/**
 * Check if an object ref matches all dimensions of a {@link TransportObjectSelector}.
 *
 * Exported so that consumers (e.g. import service) can reuse the same logic
 * without duplicating it.
 */
export function matchesSelector(
  obj: AdkTransportObjectRef,
  selector: TransportObjectSelector,
): boolean {
  return (
    matchesDimension(obj.objFunc, selector.objFunc) &&
    matchesDimension(obj.pgmid, selector.pgmid) &&
    matchesDimension(obj.type, selector.type)
  );
}

/**
 * Transport object reference - lightweight wrapper
 *
 * Contains metadata about an object in the transport.
 * Call load() to get the full ADK object.
 */
export class AdkTransportObjectRef {
  constructor(
    private readonly ctx: AdkContext,
    private readonly data: TransportObjectData,
  ) {}

  /** Program ID (R3TR, LIMU, etc.) */
  get pgmid(): string {
    return this.data.pgmid ?? '';
  }

  /** Object type (CLAS, INTF, DOMA, etc.) */
  get type(): string {
    return this.data.type ?? '';
  }

  /** Object name */
  get name(): string {
    return this.data.name ?? '';
  }

  /** Workbench type (more specific type) */
  get wbtype(): string | undefined {
    return this.data.wbtype;
  }

  /** Object URI for direct access */
  get uri(): string | undefined {
    return this.data.uri;
  }

  /** Object description */
  get description(): string | undefined {
    return this.data.obj_desc;
  }

  /**
   * Object function code (SAP OBJFUNC).
   * 'D' = marked for deletion, 'K' = key entry, '' = regular modification.
   */
  get objFunc(): string {
    return this.data.obj_func ?? '';
  }

  /** Whether this object is marked for deletion (obj_func === 'D') */
  get isDeleted(): boolean {
    return this.data.obj_func === 'D';
  }

  /** Full object key (PGMID/TYPE/NAME) */
  get key(): string {
    return `${this.pgmid}/${this.type}/${this.name}`;
  }

  /**
   * Load the full ADK object
   *
   * Uses the ADK factory to create the appropriate object type
   * (AdkClass, AdkInterface, AdkPackage, etc.)
   *
   * @returns The loaded ADK object, or undefined if type not supported
   */
  async load(): Promise<unknown> {
    // Import dynamically to avoid circular dependency
    const { createAdk } = await import('../../factory');
    const adk = createAdk(this.ctx.client);

    // Use the factory to get the object
    // The factory handles type resolution and loading
    try {
      const obj = adk.get(this.name, this.type);
      // Check if object has load method (AdkGenericObject doesn't)
      if ('load' in obj && typeof obj.load === 'function') {
        await obj.load();
      }
      return obj;
    } catch {
      // Type not supported or object not found
      return undefined;
    }
  }

  /** Raw data from API */
  get raw(): TransportObjectData {
    return this.data;
  }
}

/**
 * Transport task reference
 */
export class AdkTransportTaskRef {
  constructor(
    private readonly ctx: AdkContext,
    private readonly data: TransportTaskData,
  ) {}

  get number(): string {
    return this.data.number ?? '';
  }
  get owner(): string {
    return this.data.owner ?? '';
  }
  get description(): string {
    return this.data.desc ?? '';
  }
  get status(): string {
    return this.data.status ?? '';
  }
  get statusText(): string {
    return this.data.status_text ?? '';
  }

  /** Objects in this task */
  get objects(): AdkTransportObjectRef[] {
    return asArray(this.data.abap_object).map(
      (obj) => new AdkTransportObjectRef(this.ctx, obj),
    );
  }
}

/**
 * AdkTransport - Transport request for import operations
 *
 * Simple, focused API for importing transport objects:
 *
 * @example
 * ```typescript
 * const transport = await AdkTransport.get('DEVK900001');
 *
 * for (const objRef of transport.objects) {
 *   console.log(`${objRef.type} ${objRef.name}`);
 *   const adkObject = await objRef.load();
 *   if (adkObject) {
 *     // Serialize with plugin
 *     await plugin.serialize(adkObject);
 *   }
 * }
 * ```
 */
export class AdkTransport {
  private _objects?: AdkTransportObjectRef[];
  private _tasks?: AdkTransportTaskRef[];

  private constructor(
    private readonly ctx: AdkContext,
    private readonly data: TransportData,
    private readonly requestedNumber: string,
  ) {}

  // ===========================================================================
  // Properties
  // ===========================================================================

  /** Transport number */
  get number(): string {
    if (this.data.object_type === 'T') {
      const task = asArray(this.data.task).find(
        (candidate) => candidate.number?.toUpperCase() === this.requestedNumber,
      );
      return task?.number ?? this.requestedNumber;
    }
    return this.data.request?.number ?? this.data.name ?? this.requestedNumber;
  }

  private get itemData(): TransportTaskData {
    if (this.data.object_type === 'T') {
      const directTasks = asArray(this.data.task);
      return (
        directTasks.find(
          (task) => task.number?.toUpperCase() === this.requestedNumber,
        ) ??
        directTasks[0] ??
        this.data.request ??
        {}
      );
    }
    return this.data.request ?? {};
  }

  /** Transport description */
  get description(): string {
    return this.itemData.desc ?? '';
  }

  /** Transport owner */
  get owner(): string {
    return this.itemData.owner ?? '';
  }

  /** Transport status (D=Modifiable, R=Released) */
  get status(): string {
    return this.itemData.status ?? '';
  }

  /** Transport status text */
  get statusText(): string {
    return this.itemData.status_text ?? '';
  }

  /** Transport target system */
  get target(): string {
    return this.itemData.target ?? '';
  }

  /** Object type (K=Request, T=Task) */
  get objectType(): string {
    return this.data.object_type ?? 'K';
  }

  // ===========================================================================
  // Tasks
  // ===========================================================================

  /** Tasks belonging to this transport */
  get tasks(): AdkTransportTaskRef[] {
    if (!this._tasks) {
      const requestTasks = asArray(this.data.request?.task);
      const rootTasks = asArray(this.data.task);
      const allTasks = [...requestTasks, ...rootTasks];
      this._tasks = allTasks.map((t) => new AdkTransportTaskRef(this.ctx, t));
    }
    return this._tasks;
  }

  // ===========================================================================
  // Objects - Aggregated from all sources
  // ===========================================================================

  /**
   * All objects in this transport
   *
   * Collects objects from:
   * - Direct objects on request
   * - Objects from all tasks
   * - all_objects container (if present)
   */
  get objects(): AdkTransportObjectRef[] {
    if (!this._objects) {
      this._objects = this.collectObjects();
    }
    return this._objects;
  }

  private collectObjects(): AdkTransportObjectRef[] {
    const objects: AdkTransportObjectRef[] = [];
    const seen = new Set<string>();

    const addObject = (obj: TransportObjectData) => {
      const key = `${obj.pgmid}/${obj.type}/${obj.name}`;
      if (!seen.has(key)) {
        seen.add(key);
        objects.push(new AdkTransportObjectRef(this.ctx, obj));
      }
    };

    // Direct objects on request
    for (const obj of asArray(this.data.request?.abap_object)) {
      addObject(obj);
    }

    // Objects from all_objects container
    for (const obj of asArray(this.data.request?.all_objects?.abap_object)) {
      addObject(obj);
    }

    // Objects from tasks (both request.task and root task)
    for (const task of this.tasks) {
      for (const objRef of task.objects) {
        addObject(objRef.raw);
      }
    }

    return objects;
  }

  // ===========================================================================
  // Filtering helpers
  // ===========================================================================

  /**
   * Get objects filtered by type
   *
   * @param types - Object types to include (e.g., ['CLAS', 'INTF'])
   */
  getObjectsByType(...types: string[]): AdkTransportObjectRef[] {
    const typeSet = new Set(types.map((t) => t.toUpperCase()));
    return this.objects.filter((obj) => typeSet.has(obj.type.toUpperCase()));
  }

  /**
   * Get objects filtered by a {@link TransportObjectSelector}.
   *
   * All dimensions specified in the selector are ANDed together.
   *
   * @example
   * ```typescript
   * // Get only deletion objects with pgmid=R3TR
   * const deleted = transport.getObjectsBySelector({ objFunc: 'D', pgmid: 'R3TR' });
   * ```
   */
  getObjectsBySelector(
    selector: TransportObjectSelector,
  ): AdkTransportObjectRef[] {
    return this.objects.filter((obj) => matchesSelector(obj, selector));
  }

  /**
   * Convenience accessor for objects marked for deletion (obj_func === 'D').
   *
   * Equivalent to `getObjectsBySelector({ objFunc: 'D' })`.
   */
  get deletionObjects(): AdkTransportObjectRef[] {
    return this.getObjectsBySelector({ objFunc: 'D' });
  }

  /**
   * Get unique object types in this transport
   */
  getObjectTypes(): string[] {
    const types = new Set(this.objects.map((obj) => obj.type));
    return Array.from(types).sort();
  }

  /**
   * Get object count by type
   */
  getObjectCountByType(): Record<string, number> {
    const counts: Record<string, number> = {};
    for (const obj of this.objects) {
      counts[obj.type] = (counts[obj.type] || 0) + 1;
    }
    return counts;
  }

  // ===========================================================================
  // Raw data access
  // ===========================================================================

  /** Raw API response (unwrapped) */
  get raw(): TransportData {
    return this.data;
  }

  // ===========================================================================
  // Static Factory
  // ===========================================================================

  /**
   * Get a transport by number
   *
   * @param number - Transport number (e.g., 'DEVK900001')
   * @param ctx - Optional ADK context (uses global context if not provided)
   *
   * @example
   * ```typescript
   * const transport = await AdkTransport.get('DEVK900001');
   * console.log(`${transport.description} - ${transport.objects.length} objects`);
   * ```
   */
  static async get(number: string, ctx?: AdkContext): Promise<AdkTransport> {
    const context = ctx ?? getGlobalContext();
    const requestedNumber = number.trim().toUpperCase();
    const response =
      await context.client.adt.cts.transportrequests.get(requestedNumber);
    // Unwrap the root element from the response
    return new AdkTransport(context, response.root, requestedNumber);
  }

  /**
   * Load multiple transports and merge their objects into a single view.
   *
   * De-duplication key is `pgmid/type/name`. When the same key appears in
   * multiple transports the **first** occurrence wins (first transport in the
   * `numbers` array). The source transport number is available via
   * `AdkTransportObjectRef.sourceTransport`.
   *
   * @param numbers - Transport numbers to load and merge
   * @param ctx     - Optional ADK context
   *
   * @example
   * ```typescript
   * const merged = await AdkTransport.merge(['DEVK900001', 'DEVK900002']);
   * const deletions = merged.getObjectsBySelector({ objFunc: 'D' });
   * ```
   */
  static async merge(
    numbers: string[],
    ctx?: AdkContext,
  ): Promise<MergedTransportView> {
    const transports = await Promise.all(
      numbers.map((n) => AdkTransport.get(n, ctx)),
    );
    return new MergedTransportView(transports);
  }
}

/**
 * Result of resolving transport objects (single or multi-TR).
 *
 * Returned by {@link resolveTransportObjects}.
 */
export interface ResolvedTransportObjects {
  /** Deduplicated list of transport objects (first-win across TRs). */
  objects: AdkTransportObjectRef[];
  /** Maps each object key to the concrete request or task that supplied it. */
  sourceTransportMap: Map<string, string>;
  /** Requested roots/tasks plus observed child tasks, in stable order. */
  scopeTransportNumbers: string[];
}

function getFilteredObjects(
  transport: AdkTransport,
  selector: TransportObjectSelector,
): AdkTransportObjectRef[] {
  return Object.keys(selector).length > 0
    ? transport.getObjectsBySelector(selector)
    : transport.objects;
}

function addResolvedObject(
  object: AdkTransportObjectRef,
  sourceTransport: string,
  objects: AdkTransportObjectRef[],
  sourceTransportMap: Map<string, string>,
  seenKeys?: Set<string>,
): void {
  if (seenKeys && seenKeys.has(object.key)) return;
  if (seenKeys) seenKeys.add(object.key);
  objects.push(object);
  sourceTransportMap.set(object.key, sourceTransport);
}

function getTransportObjectKey(object: TransportObjectData): string {
  return `${object.pgmid ?? ''}/${object.type ?? ''}/${object.name ?? ''}`;
}

function getConcreteSourceTransportMap(
  transport: AdkTransport,
  requestedNumber: string,
): Map<string, string> {
  const sourceTransportMap = new Map<string, string>();
  const data = transport.raw;
  const requestNumber =
    data.request?.number || transport.number || requestedNumber;

  const addSource = (
    object: TransportObjectData,
    sourceTransport: string,
  ): void => {
    const key = getTransportObjectKey(object);
    if (!sourceTransportMap.has(key)) {
      sourceTransportMap.set(key, sourceTransport);
    }
  };

  // Direct request objects have concrete request provenance.
  for (const object of asArray(data.request?.abap_object)) {
    addSource(object, requestNumber);
  }

  // Prefer concrete task provenance over the aggregate all_objects view.
  const tasks = [...asArray(data.request?.task), ...asArray(data.task)];
  for (const task of tasks) {
    const taskNumber = task.number || transport.number || requestedNumber;
    for (const object of asArray(task.abap_object)) {
      addSource(object, taskNumber);
    }
  }

  // all_objects is an aggregate without task attribution, so use it only as
  // a fallback for keys not exposed by the concrete request/task containers.
  for (const object of asArray(data.request?.all_objects?.abap_object)) {
    addSource(object, requestNumber);
  }

  return sourceTransportMap;
}

function addScopeTransportNumber(
  number: string,
  scopeTransportNumbers: string[],
  seenScopeTransportNumbers: Set<string>,
): void {
  if (!number || seenScopeTransportNumbers.has(number)) return;
  seenScopeTransportNumbers.add(number);
  scopeTransportNumbers.push(number);
}

/**
 * Load and resolve objects from one or more transport numbers, with optional
 * selector-based filtering and deduplication across TRs.
 *
 * This is the shared logic used by both the CLI `cts tr objects` command and
 * the MCP `cts_transport_objects` tool — extracted here to avoid duplication.
 *
 * @param transportNumbers - Ordered list of TR numbers (first-win deduplication).
 * @param selector         - Optional dimension filter (objFunc, pgmid, type).
 * @param ctx              - Optional ADK context (uses global context if omitted).
 */
export async function resolveTransportObjects(
  transportNumbers: string[],
  selector: TransportObjectSelector,
  ctx?: AdkContext,
): Promise<ResolvedTransportObjects> {
  const objects: AdkTransportObjectRef[] = [];
  const sourceTransportMap = new Map<string, string>();
  const scopeTransportNumbers: string[] = [];
  const seenScopeTransportNumbers = new Set<string>();

  const transports = await Promise.all(
    transportNumbers.map((number) => AdkTransport.get(number, ctx)),
  );
  const seenObjectKeys = new Set<string>();

  for (const [index, transport] of transports.entries()) {
    const requestedNumber = transportNumbers[index] ?? '';
    const transportNumber = transport.number || requestedNumber;
    addScopeTransportNumber(
      transportNumber,
      scopeTransportNumbers,
      seenScopeTransportNumbers,
    );
    addScopeTransportNumber(
      transport.raw.request?.number ?? '',
      scopeTransportNumbers,
      seenScopeTransportNumbers,
    );
    for (const task of transport.tasks) {
      addScopeTransportNumber(
        task.number,
        scopeTransportNumbers,
        seenScopeTransportNumbers,
      );
    }

    const concreteSources = getConcreteSourceTransportMap(
      transport,
      requestedNumber,
    );
    for (const object of getFilteredObjects(transport, selector)) {
      const sourceTransport =
        concreteSources.get(object.key) ?? transportNumber;
      addResolvedObject(
        object,
        sourceTransport,
        objects,
        sourceTransportMap,
        seenObjectKeys,
      );
    }
  }

  return { objects, sourceTransportMap, scopeTransportNumbers };
}

/**
 * Merged view of objects from multiple transports.
 *
 * De-duplicates by `pgmid/type/name`; first occurrence wins.
 */
export class MergedTransportView {
  private readonly _objects: AdkTransportObjectRef[];
  readonly transports: AdkTransport[];

  constructor(transports: AdkTransport[]) {
    this.transports = transports;
    const seen = new Set<string>();
    const all: AdkTransportObjectRef[] = [];
    for (const tr of transports) {
      for (const obj of tr.objects) {
        if (!seen.has(obj.key)) {
          seen.add(obj.key);
          all.push(obj);
        }
      }
    }
    this._objects = all;
  }

  /** All objects across transports (deduplicated) */
  get objects(): AdkTransportObjectRef[] {
    return this._objects;
  }

  /** Objects matching a selector across all transports */
  getObjectsBySelector(
    selector: TransportObjectSelector,
  ): AdkTransportObjectRef[] {
    return this._objects.filter((obj) => matchesSelector(obj, selector));
  }

  /** Convenience: objects marked for deletion (obj_func === 'D') */
  get deletionObjects(): AdkTransportObjectRef[] {
    return this.getObjectsBySelector({ objFunc: 'D' });
  }

  /**
   * Load multiple transports and return a merged view.
   *
   * @param numbers - Transport numbers to load and merge
   * @param ctx     - Optional ADK context
   */
  static async create(
    numbers: string[],
    ctx?: AdkContext,
  ): Promise<MergedTransportView> {
    const transports = await Promise.all(
      numbers.map((n) => AdkTransport.get(n, ctx)),
    );
    return new MergedTransportView(transports);
  }
}
