/**
 * Shared utilities for MCP tool implementations.
 */

import type { McpRequestAccess } from './scope-catalogue.js';
import type { SafeExecutePolicy } from '../http/invocation.js';

export interface SearchObject {
  name?: string;
  type?: string;
  uri?: string;
  description?: string;
  packageName?: string;
}

export interface DiscoveryWorkspace {
  title?: string;
  collections: Array<{
    href: string;
    title?: string;
  }>;
}

/**
 * Minimal structural type for the ADT client subset used by resolveObjectUri.
 *
 * Using a structural (duck-typed) interface rather than importing the full
 * AdtClient type avoids coupling utils.ts to @abapify/adt-client and keeps
 * this utility testable with any object that satisfies the shape.
 */
type QuickSearchClient = {
  adt: {
    repository: {
      informationsystem: {
        search: {
          quickSearch(params: {
            query: string;
            maxResults: number;
          }): Promise<unknown>;
        };
      };
    };
  };
};

/**
 * Extract object references from various ADT search response shapes.
 */
export function extractObjectReferences(results: unknown): SearchObject[] {
  const resultsAny = results as Record<string, unknown>;
  let rawObjects: SearchObject | SearchObject[] | undefined;

  if ('objectReferences' in resultsAny && resultsAny.objectReferences) {
    const refs = resultsAny.objectReferences as {
      objectReference?: SearchObject | SearchObject[];
    };
    rawObjects = refs.objectReference;
  } else if ('objectReference' in resultsAny) {
    rawObjects = resultsAny.objectReference as SearchObject | SearchObject[];
  } else if ('mainObject' in resultsAny && resultsAny.mainObject) {
    const main = resultsAny.mainObject as {
      objectReference?: SearchObject | SearchObject[];
    };
    rawObjects = main.objectReference;
  }

  if (!rawObjects) return [];
  return Array.isArray(rawObjects) ? rawObjects : [rawObjects];
}

/**
 * Normalize discovery responses from either the typed Atom service document
 * shape or the legacy JSON mock shape used by older tests.
 */
export function extractDiscoveryWorkspaces(
  discovery: unknown,
): DiscoveryWorkspace[] {
  const result = discovery as
    | {
        service?: {
          workspace?: Array<{
            title?: string;
            collection?: Array<{ href: string; title?: string }>;
          }>;
        };
        workspaces?: Array<{
          title?: string;
          collections?: Array<{ href: string; title?: string }>;
        }>;
      }
    | undefined;

  const typedWorkspaces = result?.service?.workspace?.map((workspace) => ({
    title: workspace.title,
    collections: workspace.collection ?? [],
  }));

  if (typedWorkspaces && typedWorkspaces.length > 0) {
    return typedWorkspaces;
  }

  return (result?.workspaces ?? []).map((workspace) => ({
    title: workspace.title,
    collections: workspace.collections ?? [],
  }));
}

/**
 * Resolve ADT object URI from object type and name.
 *
 * Maps common ABAP object types to their ADT REST endpoint paths.
 * Returns undefined for unknown types (caller should fall back to search).
 */
export function resolveObjectUriFromType(
  objectType: string,
  objectName: string,
): string | undefined {
  const type = objectType.toUpperCase().split('/')[0];
  const n = encodeURIComponent(objectName.toLowerCase());
  switch (type) {
    case 'PROG':
      return `/sap/bc/adt/programs/programs/${n}`;
    case 'INCL':
      return `/sap/bc/adt/programs/includes/${n}`;
    case 'CLAS':
      return `/sap/bc/adt/oo/classes/${n}`;
    case 'INTF':
      return `/sap/bc/adt/oo/interfaces/${n}`;
    case 'FUGR':
      return `/sap/bc/adt/functions/groups/${n}`;
    case 'DOMA':
      return `/sap/bc/adt/ddic/domains/${n}`;
    case 'DTEL':
      return `/sap/bc/adt/ddic/dataelements/${n}`;
    case 'TABL':
      return `/sap/bc/adt/ddic/tables/${n}`;
    case 'TTYP':
      return `/sap/bc/adt/ddic/tabletypes/${n}`;
    case 'DEVC':
      // Package names are always uppercase in SAP ADT
      return `/sap/bc/adt/packages/${encodeURIComponent(objectName.toUpperCase())}`;
    case 'BDEF':
      return `/sap/bc/adt/bo/behaviordefinitions/${n}`;
    case 'SRVD':
      return `/sap/bc/adt/ddic/srvd/sources/${n}`;
    case 'SRVB':
      return `/sap/bc/adt/businessservices/bindings/${n}`;
    case 'ENHO':
      return `/sap/bc/adt/enhancements/enhoxhh/${n}`;
    default:
      return undefined;
  }
}

/**
 * Resolve the ADT URI for an ABAP object by name and optional type.
 *
 * Tries type-based resolution first (no network round-trip), then falls back
 * to a quickSearch if the type is unknown or not mapped. Returns undefined
 * when the object cannot be found.
 */
export async function resolveObjectUri(
  client: QuickSearchClient,
  objectName: string,
  objectType?: string,
): Promise<string | undefined> {
  if (objectType) {
    const uri = resolveObjectUriFromType(objectType, objectName);
    if (uri) return uri;
  }

  const searchResult =
    await client.adt.repository.informationsystem.search.quickSearch({
      query: objectName,
      maxResults: 10,
      ...(objectType ? { objectType } : {}),
    });

  const objects = extractObjectReferences(searchResult);
  const match = objects.find(
    (o) => String(o.name ?? '').toUpperCase() === objectName.toUpperCase(),
  );

  return match?.uri;
}

/**
 * Extract HTTP status from an error (works with AdtError and similar shapes).
 */
export function getErrorStatus(error: unknown): number | undefined {
  return error instanceof Error && 'status' in error
    ? (error as { status?: number }).status
    : undefined;
}

/**
 * A completed SAP HTTP error response is a deterministic failed execution.
 * Network, body-stream, abort, and internal parsing errors deliberately do not
 * match, because dispatch may have happened and their outcome is uncertain.
 */
export function isKnownAdtHttpFailure(error: unknown): boolean {
  const status = getErrorStatus(error);
  return (
    error instanceof Error &&
    error.name === 'AdtError' &&
    typeof status === 'number' &&
    Number.isInteger(status) &&
    status >= 400 &&
    status <= 599
  );
}

/**
 * Build a standard MCP error result with BTP 404 awareness.
 */
export function mcpErrorResult(
  error: unknown,
  toolLabel: string,
  notFoundHint?: string,
) {
  const status = getErrorStatus(error);
  const message =
    status === 404 && notFoundHint
      ? notFoundHint
      : `${toolLabel} failed: ${error instanceof Error ? error.message : String(error)}`;
  return {
    isError: true as const,
    content: [{ type: 'text' as const, text: message }],
  };
}

export type McpErrorResult = ReturnType<typeof mcpErrorResult>;

/**
 * Extract the safe-execute policy for a tool when the caller has been granted
 * a scoped safe-execute grant for this operationId.
 */
export function extractSafeExecutePolicy(
  access: McpRequestAccess | undefined,
  operationId: string,
): SafeExecutePolicy | undefined {
  return access?.scoped?.operationClass === 'safe_execute' &&
    access.scoped.safeExecutePolicy?.operationId === operationId
    ? access.scoped.safeExecutePolicy
    : undefined;
}

/**
 * For safe-execute tool handlers, rethrow non-deterministic errors when a safe
 * policy is active so the outer execution recorder can report outcome_unknown;
 * return a standard MCP error for known SAP HTTP failures.
 */
export function handleSafeExecuteError(
  error: unknown,
  safePolicy: SafeExecutePolicy | undefined,
  toolLabel: string,
): McpErrorResult {
  if (safePolicy && !isKnownAdtHttpFailure(error)) throw error;
  return mcpErrorResult(error, toolLabel);
}

/**
 * Standard MCP result returned when the active scope does not allow a tool.
 */
export function scopeDeniedResult() {
  return {
    isError: true as const,
    content: [{ type: 'text' as const, text: 'mcp_scope_denied' }],
  };
}

/**
 * Standard MCP result returned when a safe-execute guard limit is hit.
 */
export function safeExecuteLimitResult(
  text: 'safe_execute_limit_exceeded' | 'outcome_unknown',
) {
  return {
    isError: true as const,
    content: [{ type: 'text' as const, text }],
  };
}
