/**
 * AdkTransportRequest & AdkTransportTask
 *
 * Simplified 2-layer structure:
 * - AdkTransportRequest: full transport request with tasks, CRUD, etc.
 * - AdkTransportTask: extends request with task-specific overrides
 *
 * Both share the same API response structure, just different data sources.
 * Kept in one file to avoid circular dependency issues.
 *
 * Architecture:
 * - Uses ADT contracts directly via ctx.client.adt.cts.*
 * - No intermediate "service" layer - ADK objects ARE the service layer
 * - Raw fetch for operations not covered by contracts (create, release, lock)
 */

import type { AdkContext } from '../../../base/context';
import { getGlobalContext } from '../../../base/global-context';
import { AdkObject } from '../../../base/model';
import { TransportRequest as TransportRequestKind } from '../../../base/kinds';
import { requiresLock, parseSapTimestamp } from '../../../decorators';
import type {
  TransportData,
  TransportRequestData,
  TransportTaskData,
  TransportObjectData,
  TransportCreateOptions,
  TransportUpdateOptions,
  ReleaseResult,
} from './transport.types';
import { AdkTransportObject } from './transport-object';

// =============================================================================
// Helpers
// =============================================================================

const STATUS_TEXT: Record<string, string> = {
  D: 'Modifiable',
  R: 'Released',
  N: 'Not released',
};

function asArray<T>(val: T | T[] | undefined): T[] {
  if (!val) return [];
  return Array.isArray(val) ? val : [val];
}

function transportResponseName(response: TransportData): string {
  if (response.object_type === 'T') {
    const taskNumber = asArray(response.task)[0]?.number;
    if (taskNumber) return taskNumber;
  }
  if (response.name) return response.name;
  return response.request?.number || '';
}

type TransportReleaseResponse = Awaited<
  ReturnType<
    AdkContext['client']['adt']['cts']['transportrequests']['useraction']['release']
  >
>;

function releaseReportFailure(
  response: TransportReleaseResponse,
): string | undefined {
  const reports = asArray(response.root.releasereports?.checkReport);
  if (reports.length === 0) {
    return 'SAP release response did not contain a release report';
  }

  const failed = reports.find(
    (report) => report.status?.toLowerCase() !== 'released',
  );
  if (!failed) return undefined;

  const message = asArray(failed.checkMessageList?.checkMessage).find(
    (entry) => entry.shortText,
  )?.shortText;
  return (
    message ||
    failed.statusText ||
    `SAP release report failed with status ${failed.status || 'unknown'}`
  );
}

// =============================================================================
// Config Cache (module-level for efficiency)
// =============================================================================

let cachedConfigUri: string | undefined;

async function getConfigUri(ctx: AdkContext): Promise<string> {
  if (cachedConfigUri) return cachedConfigUri;

  const response =
    await ctx.client.adt.cts.transportrequests.searchconfiguration.configurations.get();
  const configs = response?.configurations;
  if (!configs) throw new Error('No search configuration found');

  const configArray = Array.isArray(configs) ? configs : [configs];
  if (configArray.length === 0)
    throw new Error('No search configuration found');

  const uri = configArray[0]?.link?.href;
  if (!uri) throw new Error('No search configuration URI found');

  cachedConfigUri = uri;
  return uri;
}

/** Clear config cache (for testing) */
export function clearConfigCache(): void {
  cachedConfigUri = undefined;
}

// =============================================================================
// Response Normalization Helpers
// =============================================================================

interface RawRequest {
  number?: string;
  owner?: string;
  desc?: string;
  status?: string;
  uri?: string;
  task?: unknown;
  abap_object?: unknown;
}

/**
 * Collect all requests from the transport response structure
 * The schema has requests in multiple places:
 * - workbench.modifiable.request[]
 * - workbench.relstarted.request[]
 * - workbench.released.request[]
 * - workbench.target[].modifiable.request[]
 * - customizing.* (same structure)
 */
function collectAllRequests(result: unknown): RawRequest[] {
  const requests: RawRequest[] = [];

  if (!result || typeof result !== 'object') return requests;

  const data = result as Record<string, unknown>;

  // Helper to extract requests from a container (modifiable, relstarted, released)
  const extractFromContainer = (container: unknown) => {
    if (!container || typeof container !== 'object') return;
    const c = container as Record<string, unknown>;
    const reqs = c.request;
    if (reqs) {
      const reqArray = Array.isArray(reqs) ? reqs : [reqs];
      requests.push(...(reqArray as RawRequest[]));
    }
  };

  // Helper to process workbench or customizing section
  const processSection = (section: unknown) => {
    if (!section || typeof section !== 'object') return;
    const s = section as Record<string, unknown>;

    // Direct containers
    extractFromContainer(s.modifiable);
    extractFromContainer(s.relstarted);
    extractFromContainer(s.released);

    // Target-specific containers
    const targets = s.target;
    if (targets) {
      const targetArray = Array.isArray(targets) ? targets : [targets];
      for (const target of targetArray) {
        if (target && typeof target === 'object') {
          const t = target as Record<string, unknown>;
          extractFromContainer(t.modifiable);
          extractFromContainer(t.relstarted);
          extractFromContainer(t.released);
        }
      }
    }
  };

  processSection(data.workbench);
  processSection(data.customizing);

  return requests;
}

// =============================================================================
// Internal Data Type
// =============================================================================

interface TransportObjectDataInternal {
  name: string;
  type: string;
  response: TransportData;
}

// =============================================================================
// AdkTransportRequest
// =============================================================================

/**
 * Transport Request - main transport object
 *
 * Handles both requests (K) and can be extended for tasks (T).
 * Uses ADT contracts directly - no intermediate service layer.
 */
export class AdkTransportRequest extends AdkObject<
  typeof TransportRequestKind,
  TransportObjectDataInternal
> {
  readonly kind = TransportRequestKind;
  protected _tasks?: AdkTransportTask[];
  protected _objects?: AdkTransportObject[];

  constructor(ctx: AdkContext, dataOrNumber: TransportData | string) {
    if (typeof dataOrNumber === 'string') {
      super(ctx, dataOrNumber);
    } else {
      super(ctx, {
        name: transportResponseName(dataOrNumber),
        type: dataOrNumber.object_type === 'T' ? 'RQTQ' : 'RQRQ',
        response: dataOrNumber,
      });
    }
  }

  // ===========================================================================
  // Response Access
  // ===========================================================================

  protected get response(): TransportData {
    return this.dataSync.response;
  }

  /** The item's data - override in subclass for different source */
  protected get itemData(): TransportRequestData {
    return this.response.request || ({} as TransportRequestData);
  }

  get objectUri(): string {
    return `/sap/bc/adt/cts/transportrequests/${this.name}`;
  }

  override async load(): Promise<this> {
    const rawResponse = await this.ctx.client.adt.cts.transportrequests.get(
      this.name,
    );
    // Unwrap the root element from the response
    const response = rawResponse.root;
    this.setData({
      name: this.name,
      type: response.object_type === 'T' ? 'RQTQ' : 'RQRQ',
      response,
    });
    return this;
  }

  // Note: save() not supported - no crudContract defined
  // Transport requests are managed via CTS APIs (release, import, etc.)

  // ===========================================================================
  // Properties (shared by request and task)
  // ===========================================================================

  get itemType(): 'request' | 'task' {
    return this.response.object_type === 'T' ? 'task' : 'request';
  }

  get number(): string {
    return this.name;
  }
  get owner(): string {
    return this.itemData.owner || '';
  }
  override get description(): string {
    return this.itemData.desc || '';
  }
  get status(): string {
    return this.itemData.status || 'D';
  }
  get statusText(): string {
    return this.itemData.status_text || STATUS_TEXT[this.status] || this.status;
  }
  get target(): string {
    return this.itemData.target || '';
  }
  get targetDescription(): string {
    return this.itemData.target_desc || '';
  }
  get uri(): string {
    return this.itemData.uri || this.objectUri;
  }
  get parent(): string {
    return this.itemData.parent || '';
  }

  get lastChangedAt(): Date | undefined {
    return parseSapTimestamp(this.itemData.lastchanged_timestamp);
  }

  // ===========================================================================
  // Composition: Tasks (request only)
  // ===========================================================================

  /** Tasks belonging to this transport request */
  get tasks(): AdkTransportTask[] {
    if (!this._tasks) {
      const taskData = this.itemData.task;
      this._tasks = asArray(taskData).map((t) => {
        // Build response for task
        const { request, task: _existingTasks, ...rootProps } = this.response;
        const { task: _nestedTasks, ...requestWithoutTasks } = request || {};

        const taskResponse: TransportData = {
          ...rootProps,
          object_type: 'T',
          name: t.number || '',
          request: request
            ? (requestWithoutTasks as typeof request)
            : undefined,
          task: [t],
        };
        return new AdkTransportTask(this.ctx, taskResponse, this);
      });
    }
    return this._tasks;
  }

  // ===========================================================================
  // Composition: Objects
  // ===========================================================================

  /**
   * All objects in this request (aggregated from tasks)
   * Override in AdkTransportTask for direct objects
   */
  get objects(): AdkTransportObject[] {
    if (!this._objects) {
      // Direct objects in request
      const directObjects = asArray(this.itemData.abap_object).map(
        (o) => new AdkTransportObject(o),
      );
      // Objects from tasks
      const taskObjects = this.tasks.flatMap((t) => t.objects);
      this._objects = [...directObjects, ...taskObjects];
    }
    return this._objects;
  }

  // ===========================================================================
  // Actions
  // ===========================================================================

  async release(): Promise<ReleaseResult> {
    try {
      const response =
        await this.ctx.client.adt.cts.transportrequests.useraction.release(
          this.number,
        );
      const reportFailure = releaseReportFailure(response);
      if (reportFailure) return { success: false, message: reportFailure };

      const verified = await AdkTransportRequest.get(this.number, this.ctx);
      if (verified.status !== 'R') {
        return {
          success: false,
          message: `Release verification failed for ${this.number}: transport still has status ${verified.status}`,
        };
      }

      (this.itemData as { status?: string; status_text?: string }).status =
        verified.status;
      (this.itemData as { status?: string; status_text?: string }).status_text =
        verified.statusText;
      return { success: true };
    } catch (error) {
      return {
        success: false,
        message: error instanceof Error ? error.message : String(error),
      };
    }
  }

  async releaseAll(): Promise<ReleaseResult> {
    for (const task of this.tasks) {
      if (task.status !== 'R') {
        const result = await task.release();
        if (!result.success) {
          return {
            success: false,
            message: `Failed to release task ${task.number}: ${result.message}`,
          };
        }
      }
    }
    return this.release();
  }

  // ===========================================================================
  // CRUD Operations (request only)
  // ===========================================================================

  @requiresLock()
  async update(changes: TransportUpdateOptions): Promise<void> {
    await this.ctx.client.adt.cts.transportrequests.put(this.number, {
      desc: changes.description ?? this.description,
      target: changes.target ?? this.target,
    } as Parameters<typeof this.ctx.client.adt.cts.transportrequests.put>[1]);

    if (changes.description !== undefined) {
      (this.itemData as { desc?: string }).desc = changes.description;
    }
    if (changes.target !== undefined) {
      (this.itemData as { target?: string }).target = changes.target;
    }
  }

  async delete(): Promise<void> {
    await this.ctx.client.adt.cts.transportrequests.delete(this.number);
  }

  /**
   * Reassign ownership of this transport (and optionally all tasks) to a new user.
   *
   * @param newOwner - SAP username to become the new owner
   * @param recursive - When true, also reassign all modifiable tasks
   */
  async reassign(newOwner: string, recursive = false): Promise<void> {
    await this.ctx.client.adt.cts.transportrequests.useraction.reassign(
      this.number,
      { targetUser: newOwner },
      {
        root: {
          number: this.number,
          useraction: 'changeowner',
          targetuser: newOwner,
        },
      },
    );

    const verified = await AdkTransportRequest.get(this.number, this.ctx);
    if (verified.owner.toUpperCase() !== newOwner.toUpperCase()) {
      throw new Error(
        `Transport ${this.number} owner verification failed: expected ${newOwner}, found ${verified.owner || '<empty>'}`,
      );
    }

    (this.itemData as { owner?: string }).owner = verified.owner;

    if (recursive) {
      for (const task of this.tasks) {
        if (task.status === 'D') {
          await task.reassign(newOwner);
        }
      }
    }
  }

  /** Create and verify a modifiable task under this request. */
  async addTask(owner: string): Promise<AdkTransportTask> {
    if (this.itemType !== 'request') {
      throw new Error(`Transport ${this.number} is a task, not a request`);
    }
    if (this.status !== 'D') {
      throw new Error(
        `Transport ${this.number} is not modifiable: expected status D, found ${this.status}`,
      );
    }

    const existingTasks = new Set(this.tasks.map((task) => task.number));
    const response =
      await this.ctx.client.adt.cts.transportrequests.useraction.addTask(
        this.number,
        { owner },
        { root: { targetuser: owner } },
      );

    const returnedNumber = response.root.number;
    if (!returnedNumber) {
      throw new Error(
        `Task creation verification failed for ${this.number}: SAP response did not contain a task number`,
      );
    }
    const verified = await AdkTransportRequest.get(this.number, this.ctx);
    const created = verified.tasks.find(
      (task) =>
        task.number === returnedNumber &&
        !existingTasks.has(task.number) &&
        task.owner.toUpperCase() === owner.toUpperCase(),
    );
    if (!created) {
      throw new Error(
        `Task creation verification failed for ${this.number}: no new task owned by ${owner}`,
      );
    }
    if (created.status !== 'D') {
      throw new Error(
        `Task creation verification failed for ${created.number}: expected status D, found ${created.status}`,
      );
    }
    this._tasks = verified.tasks;
    this._objects = undefined;
    return created;
  }

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

  /**
   * Create a new transport request
   *
   * @param options - Transport creation options
   * @param ctx - Optional ADK context (uses global context if not provided)
   */
  static async create(
    options: TransportCreateOptions,
    ctx?: AdkContext,
  ): Promise<AdkTransportRequest> {
    const context = ctx ?? getGlobalContext();

    // Get owner - use provided or auto-detect from system
    let owner = options.owner;
    if (!owner) {
      owner = await AdkTransportRequest.getCurrentUser(context);
    }

    const response =
      await context.client.adt.cts.transportrequests.useraction.create(
        {
          description: options.description,
          type: options.type,
          target: options.target,
          project: options.project,
          owner,
        },
        {
          root: {
            useraction: 'newrequest',
            request: {
              desc: options.description,
              type: options.type || 'K',
              target: options.target || 'LOCAL',
              cts_project: options.project || '',
              task: [{ owner }],
            },
          },
        },
      );

    // Parse response - extract transport number.
    // SAP returns the new transport in root.request.number (preferred) or root.name.
    const number =
      response?.root?.request?.number ?? response?.root?.name ?? undefined;
    if (!number)
      throw new Error('Failed to create transport - no number returned');

    return AdkTransportRequest.get(number, context);
  }

  /**
   * Get a transport request by number
   *
   * @param number - Transport number (e.g., 'TRLK900001')
   * @param ctx - Optional ADK context (uses global context if not provided)
   */
  static async get(
    number: string,
    ctx?: AdkContext,
  ): Promise<AdkTransportRequest> {
    const context = ctx ?? getGlobalContext();
    const rawResponse =
      await context.client.adt.cts.transportrequests.get(number);
    // Unwrap the root element from the response
    const response = rawResponse.root;
    // Return task or request based on object_type
    if (response.object_type === 'T') {
      return new AdkTransportTask(context, response);
    }
    return new AdkTransportRequest(context, response);
  }

  /**
   * List all transports for the current user
   *
   * @param ctx - Optional ADK context (uses global context if not provided)
   */
  static async list(ctx?: AdkContext): Promise<AdkTransportRequest[]> {
    const context = ctx ?? getGlobalContext();
    const configUri = await getConfigUri(context);

    const response = await context.client.adt.cts.transportrequests.list({
      targets: 'true',
      configUri: configUri,
    });

    const requests = collectAllRequests(response);
    // Wrap raw request data in TransportData structure expected by constructor
    return requests.map(
      (r) =>
        new AdkTransportRequest(context, {
          name: r.number || '',
          object_type: 'K',
          request: {
            number: r.number,
            owner: r.owner,
            desc: r.desc,
            status: r.status,
            uri: r.uri,
            task: r.task as TransportTaskData | TransportTaskData[] | undefined,
            abap_object: r.abap_object as
              TransportObjectData | TransportObjectData[] | undefined,
          },
        } as TransportData),
    );
  }

  /**
   * Get current user from the system metadata
   *
   * @param ctx - Optional ADK context (uses global context if not provided)
   */
  static async getCurrentUser(ctx?: AdkContext): Promise<string> {
    const context = ctx ?? getGlobalContext();

    const response =
      await context.client.adt.cts.transportrequests.searchconfiguration.metadata.get();

    const properties = response?.configuration?.properties?.property ?? [];
    const userProp = properties.find((p: { key?: string }) => p.key === 'User');
    const user = userProp?.$value?.trim();
    if (user) {
      return user;
    }

    throw new Error('Could not detect current user from metadata response');
  }

  // ===========================================================================
  // Lock Operations — inherited from base class via ctx.lockService.
  // Transport objectUri is /sap/bc/adt/cts/transportrequests/{number}
  // which the lock service uses directly.
  // ===========================================================================
}

// =============================================================================
// AdkTransportTask
// =============================================================================

/**
 * Transport Task - child of a transport request
 *
 * Minimal overrides:
 * - itemData: from response.task instead of response.request
 * - objects: direct from task, not aggregated
 * - request: parent reference
 * - No tasks, no CRUD
 */
export class AdkTransportTask extends AdkTransportRequest {
  private _parentRequest?: AdkTransportRequest;
  private readonly parentRef?: AdkTransportRequest;

  constructor(
    ctx: AdkContext,
    dataOrNumber: TransportData | string,
    parentRef?: AdkTransportRequest,
  ) {
    super(ctx, dataOrNumber);
    this.parentRef = parentRef;
  }

  // ===========================================================================
  // Override: itemData from task element
  // ===========================================================================

  protected override get itemData(): TransportRequestData {
    const rootTask = this.response.task;
    if (rootTask) {
      const taskArr = Array.isArray(rootTask) ? rootTask : [rootTask];
      const found = taskArr.find((t) => t.number === this.name) || taskArr[0];
      if (found) return found as unknown as TransportRequestData;
    }
    return this.response.request || ({} as TransportRequestData);
  }

  // ===========================================================================
  // Override: No tasks
  // ===========================================================================

  override get tasks(): AdkTransportTask[] {
    return []; // Tasks don't have sub-tasks
  }

  // ===========================================================================
  // Override: Direct objects (not aggregated)
  // ===========================================================================

  override get objects(): AdkTransportObject[] {
    if (!this._objects) {
      const taskData = this.itemData as unknown as TransportTaskData;
      this._objects = asArray(taskData?.abap_object).map(
        (o) => new AdkTransportObject(o),
      );
    }
    return this._objects;
  }

  // ===========================================================================
  // Task-specific: Parent reference
  // ===========================================================================

  /** Parent transport request */
  get request(): AdkTransportRequest {
    if (this.parentRef) return this.parentRef;

    if (!this._parentRequest) {
      // Parent request number comes from response.request.number or task's parent attribute
      const parentNumber = this.response.request?.number || this.parent;
      this._parentRequest = new AdkTransportRequest(this.ctx, {
        ...this.response,
        name: parentNumber,
        object_type: 'K',
      });
    }
    return this._parentRequest;
  }

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

  /**
   * Get a transport task by number
   *
   * @param number - Task number (e.g., 'TRLK900001')
   * @param ctx - Optional ADK context (uses global context if not provided)
   */
  static override async get(
    number: string,
    ctx?: AdkContext,
  ): Promise<AdkTransportTask> {
    const context = ctx ?? getGlobalContext();
    const rawResponse =
      await context.client.adt.cts.transportrequests.get(number);
    // Unwrap the root element from the response
    return new AdkTransportTask(context, rawResponse.root);
  }
}
