// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { AppAction, ActionResult, TypeAgentAction } from "./action.js";
import { AppAgentCommandInterface } from "./command.js";
import {
    ActionIO,
    DisplayType,
    DynamicDisplay,
    DisplayContent,
} from "./display.js";
import { Entity } from "./memory.js";
import { Profiler } from "./profiler.js";
import { TemplateSchema } from "./templateInput.js";

//==============================================================================
// Indexing Service Types
//==============================================================================
export type IndexingServiceConfig = {
    serviceScript: string;
    description?: string;
};

export type IndexingServicesManifest = Record<string, IndexingServiceConfig>;

// if "separate", each activity is cache separately, or if "shared", it will share the cache with as if no activity is active.
// If true, default to "separate".
// If false, cache is disabled.
export type ActivityCacheSpec = "separate" | "shared" | boolean;

//==============================================================================
// Manifest
//==============================================================================
export type AppAgentManifest = {
    emojiChar: string;
    description: string;
    commandDefaultEnabled?: boolean;
    localView?: boolean; // whether the agent serve a local view, default is false
    sharedLocalView?: string[]; // list of agents to share the local view with, default is none
    indexingServices?: IndexingServicesManifest;
    cachedActivities?: Record<string, ActivityCacheSpec>; // Key is activity name, default (if not specified) is false
    // Registered flow programs: actionName → path to .flow.json (relative to manifest file)
    flows?: Record<string, string>;
    allowDynamicAgents?: boolean; // whether this agent can add/remove dynamic sub-agents at runtime, default is false
} & ActionManifest;

export type SchemaTypeNames = {
    action?: string;
    activity?: string;
    entities?: string;
};

export type SchemaFormat = "ts" | "pas";
export type GrammarFormat = "ag" | "agr";

export type SchemaContent = {
    format: SchemaFormat;
    // TODO: enable non-stringify pas content.
    content: string;
    config?: string | undefined; // for "ts" only
};
export type GrammarContent = { format: GrammarFormat; content: string };

export type SchemaManifest = {
    description: string;
    schemaType: string | SchemaTypeNames; // string if there are only action schemas
    schemaFile: string | SchemaContent; // path relative to the manifest file (resolved to absolute at load time)
    originalSchemaFile?: string; // path to the original .ts schema source for code paths not yet converted to use .pas format (relative to the manifest file)
    grammarFile?: string | GrammarContent; // path relative to the manifest file (resolved to absolute at load time)
    injected?: boolean; // whether the translator is injected into other domains, default is false
    cached?: boolean; // whether the translator's action should be cached, default is true
    streamingActions?: string[];
};

export type ActionManifest = {
    defaultEnabled?: boolean;
    schemaDefaultEnabled?: boolean;
    actionDefaultEnabled?: boolean;
    transient?: boolean; // whether the translator is transient, default is false
    errorReasoning?: boolean; // invoke reasoning on action error, default is false

    schema?: SchemaManifest;
    subActionManifests?: { [key: string]: ActionManifest };
};

//==============================================================================
// App Agent
//==============================================================================

export type AppAgentInitSettings = {
    localHostPort?: number; // the assigned port to use to serve the view if localHostPort is true in the manifest
    options?: unknown; // additional options specific for the agent initialization
};

export type ResolveEntityResult = {
    match: "exact" | "fuzzy";
    entities: Entity[];
};

export interface AppAgent extends Partial<AppAgentCommandInterface> {
    // Setup
    initializeAgentContext?(settings?: AppAgentInitSettings): Promise<unknown>;
    updateAgentContext?(
        enable: boolean,
        context: SessionContext,
        schemaName: string, // for sub-action schemas
    ): Promise<void>;
    closeAgentContext?(context: SessionContext): Promise<void>;

    // Actions
    streamPartialAction?(
        actionName: string,
        name: string,
        value: string,
        delta: string | undefined,
        context: ActionContext<unknown>,
    ): void;
    executeAction?(
        action: TypeAgentAction,
        context: ActionContext<unknown>,
    ): Promise<ActionResult | undefined>;

    // Choice (yes/no confirmation or multi-select)
    handleChoice?(
        choiceId: string,
        response: boolean | number[],
        context: ActionContext<unknown>,
    ): Promise<ActionResult | undefined>;

    // Cache extensions
    validateWildcardMatch?(
        action: AppAction,
        context: SessionContext,
    ): Promise<boolean>;

    // Input
    resolveEntity?(
        type: string,
        name: string,
        context: SessionContext,
    ): Promise<ResolveEntityResult | undefined>;
    getTemplateSchema?(
        templateName: string,
        data: unknown,
        context: SessionContext,
    ): Promise<TemplateSchema>;
    getTemplateCompletion?(
        templateName: string,
        data: unknown,
        propertyName: string,
        context: SessionContext,
    ): Promise<string[] | undefined>;
    // For action completion (template, request/action command  completion)
    getActionCompletion?(
        context: SessionContext,
        partialAction: AppAction, // action schemaName and actionName are validated by the dispatcher.
        propertyName: string,
        entityTypeName?: string, // the type of the entity if the property is an entity
    ): Promise<string[] | undefined>;
    // Output
    getDynamicDisplay?(
        type: DisplayType,
        dynamicDisplayId: string,
        context: SessionContext,
    ): Promise<DynamicDisplay>;

    // Dynamic schema/grammar — allows agents to modify their schema and grammar at runtime.
    // Called by the dispatcher during updateAgentContext and when the agent calls reloadAgentSchema().
    getDynamicSchema?(
        context: SessionContext,
        schemaName: string,
    ): Promise<SchemaContent | undefined>;
    getDynamicGrammar?(
        context: SessionContext,
        schemaName: string,
    ): Promise<GrammarContent | undefined>;
}

//==============================================================================
// Context
//==============================================================================
export enum AppAgentEvent {
    Error = "error",
    Warning = "warning",
    Info = "info",
    Debug = "debug",

    // Display-focused events
    Toast = "toast",
    Inline = "inline",
}

export interface SessionContext<T = unknown> {
    readonly agentContext: T;
    readonly sessionStorage: Storage | undefined;
    readonly instanceStorage: Storage | undefined; // storage that are preserved across sessions

    notify(
        event: AppAgentEvent,
        message: string | DisplayContent,
        notificationId?: string,
    ): void;

    // choices default to ["Yes", "No"]
    popupQuestion(
        message: string,
        choices?: string[],
        defaultId?: number,
    ): Promise<number>;

    // can only toggle the sub agent of the current agent
    toggleTransientAgent(agentName: string, active: boolean): Promise<void>;

    // Only for selected agents (browser) can dynamically add agent. Throw if not permitted.
    addDynamicAgent(
        agentName: string,
        manifest: AppAgentManifest,
        appAgent: AppAgent,
    ): Promise<void>;

    removeDynamicAgent(agentName: string): Promise<void>;

    forceCleanupDynamicAgent(agentName: string): Promise<void>;

    // Notify the dispatcher that this agent's schema and/or grammar has changed.
    // The dispatcher will call getDynamicSchema/getDynamicGrammar to get the updated content.
    reloadAgentSchema(): Promise<void>;

    // Experimental: get the shared local host port
    getSharedLocalHostPort(agentName: string): Promise<number>;

    // Experimental: update this agent's bound local host port (used after OS port assignment)
    setLocalHostPort(port: number): void;

    // Experimental: get the available indexes
    indexes(type: "image" | "email" | "website" | "all"): Promise<any[]>;
}

// TODO: only utf8 & base64 is supported for now.
export type StorageEncoding = "utf8" | "base64";

export type StorageListOptions = {
    dirs?: boolean;
    fullPath?: boolean;
};

export interface TokenCachePersistence {
    load(): Promise<string | null>;
    save(token: string): Promise<void>;
    delete(): Promise<boolean>;
}

export interface Storage {
    read(storagePath: string): Promise<Uint8Array>;
    read(storagePath: string, options: StorageEncoding): Promise<string>;
    write(
        storagePath: string,
        data: string,
        options?: StorageEncoding, // default is utf8
    ): Promise<void>;
    write(storagePath: string, data: Uint8Array): Promise<void>;
    list(storagePath: string, options?: StorageListOptions): Promise<string[]>;
    exists(storagePath: string): Promise<boolean>;
    delete(storagePath: string): Promise<void>;

    getTokenCachePersistence(): Promise<TokenCachePersistence>;
}

export type ActivityContext<T = Record<string, unknown>> = {
    appAgentName: string;
    activityName: string;
    description: string;
    state: T;
    openLocalView?: boolean | undefined;
    activityEndAction?: AppAction | undefined;
    restricted?: boolean | undefined; // restrict the actions to this specific agent, default is false
};

export interface ActionContext<T = void> {
    profiler?: Profiler | undefined;
    streamingContext: unknown;
    readonly activityContext: ActivityContext | undefined;
    readonly actionIO: ActionIO;
    readonly sessionContext: SessionContext<T>;
    readonly abortSignal?: AbortSignal | undefined;

    // true when this action was dispatched from within the reasoning loop (via MCP execute_action),
    // false when dispatched directly from the translator. Agents can use this to decide whether
    // to execute immediately or redirect back to the reasoning loop.
    readonly isFromReasoningLoop: boolean;

    // queue up toggle transient agent to be executed at the end of the commands
    queueToggleTransientAgent(
        agentName: string,
        active: boolean,
    ): Promise<void>;
}
