import type {
  BackgroundCompletion,
  JsonValue,
  PluginCommandTask,
  PluginPublicEvent,
  PluginPublicEventType,
} from "@/domain";
import type {
  BackgroundTaskAttachment,
  FocusedProject,
  FocusedProjectGateway,
  OpenTerminalOptions,
  PluginConfigGateway,
  PluginEventGateway,
  PluginProjectStructureQuery,
  PluginTaskGateway,
  PluginToolchainGateway,
  PluginWorkspaceGateway,
  ProjectGateway,
  TerminalGateway,
  WindowGateway,
  WorkStateGateway,
} from "@/ports";

export interface PluginServices {
  workspace: WorkspaceService;
  tasks: BackgroundTaskService;
  tooling: ToolingService;
  events: EventService;
  config: ConfigDocumentService;
  terminal: TerminalService;
  windows: WindowService;
}

export interface WorkspaceProject {
  id: string;
  name: string;
  root: string;
}

export interface WorkspaceService {
  getCurrentProject(): Promise<WorkspaceProject | null>;
  getProjectRoot(projectId?: string): Promise<string>;
  readProjectContext(projectId?: string): Promise<string>;
  updateProjectContext(content: string, projectId?: string): Promise<void>;
  resolvePath(path: string, projectId?: string): Promise<WorkspaceResolvedPath>;
  readTextFile(path: string, projectId?: string): Promise<WorkspaceTextFile>;
  readBinaryFile(path: string, projectId?: string): Promise<WorkspaceBinaryFile>;
  writeTextFile(path: string, content: string, projectId?: string): Promise<void>;
  writeBinaryFile(path: string, bytes: Uint8Array, projectId?: string): Promise<void>;
  listDirectory(path?: string, projectId?: string): Promise<WorkspaceDirectoryListing>;
  stat(path: string, projectId?: string): Promise<WorkspaceStat>;
  watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
  queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
}

export interface WorkspaceResolvedPath {
  projectId: string;
  root: string;
  path: string;
}

export interface WorkspaceTextFile {
  path: string;
  content: string;
}

export interface WorkspaceBinaryFile {
  path: string;
  bytes: Uint8Array;
}

export interface WorkspaceDirEntry {
  name: string;
  path: string;
  isDir: boolean;
}

export interface WorkspaceDirectoryListing {
  path: string;
  entries: WorkspaceDirEntry[];
}

export interface WorkspaceStat {
  path: string;
  exists: boolean;
  isFile: boolean;
  isDir: boolean;
  len: number | null;
}

export interface WorkspaceWatchEvent {
  path: string;
  kind: "created" | "modified" | "deleted" | "renamed" | "unknown";
  operation: string;
  projectId: string;
}

export type WorkspaceWatchHandler = (event: WorkspaceWatchEvent) => void;

export interface WorkspaceWatch {
  dispose(): void;
}

export interface WorkspaceStructureQuery {
  projectId?: string;
  path?: string;
  maxDepth?: number;
  maxEntries?: number;
}

export type ProjectStructureEntryKind = "file" | "directory";

export interface ProjectStructureEntry {
  path: string;
  name: string;
  kind: ProjectStructureEntryKind;
}

export interface ProjectConvention {
  id: string;
  markerPath: string;
}

export interface ProjectModule {
  path: string;
  markerPath: string;
  conventionId: string;
}

export interface ProjectStructure {
  projectId: string;
  rootPath: string;
  entries: ProjectStructureEntry[];
  conventions: ProjectConvention[];
  modules: ProjectModule[];
  truncated: boolean;
}

export interface BackgroundTaskStatus {
  taskId: string;
  ownerAgentId: string;
  projectId: string;
  kind: string;
  status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
  exitCode: number | null;
  summary: string | null;
  stdoutTail: string | null;
  stderrTail: string | null;
  updatedAtMs: number;
}

export interface BackgroundTaskOutputAttachment {
  taskId: string;
  scrollback: Uint8Array;
  live: boolean;
  detach(): void;
}

export interface BackgroundTaskRetryResult {
  taskId?: string;
}

export interface RunCommandTaskOptions {
  projectId?: string;
  ownerAgentId: string;
  label?: string;
  command: string;
  args?: string[];
  cwd?: string;
  env?: Record<string, string> | Array<[string, string]>;
  recordOnly?: boolean;
  deadlineMs?: number;
}

export interface CommandTaskStatus {
  taskId: string;
  ownerAgentId: string;
  projectId: string;
  kind: string;
  state: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "expired";
  exitCode: number | null;
  summary: string | null;
  stdoutTail: string | null;
  stderrTail: string | null;
  createdAtMs: number;
  updatedAtMs: number;
}

export interface ToolRequirement {
  id: string;
  executable: string;
  versionArgs?: string[];
  required?: boolean;
  env?: Record<string, string> | Array<[string, string]>;
}

export interface EnvRequirement {
  name: string;
  required?: boolean;
  equals?: string;
}

export interface FileRequirement {
  path: string;
  required?: boolean;
  kind?: "file" | "directory" | "any";
}

export interface ToolchainDiagnosticRequest {
  projectId?: string;
  cwd?: string;
  tools?: ToolRequirement[];
  env?: EnvRequirement[];
  files?: FileRequirement[];
}

export interface ToolchainDiagnostic {
  projectId: string;
  cwd: string;
  ok: boolean;
  tools: ToolDiagnostic[];
  env: EnvDiagnostic[];
  files: FileDiagnostic[];
  messages: DiagnosticMessage[];
}

export interface ToolDiagnostic {
  id: string;
  executable: string;
  present: boolean;
  ok: boolean;
  status: "ok" | "failed" | "missing";
  required: boolean;
  exitCode: number | null;
  version: string | null;
  stdout: string | null;
  stderr: string | null;
  error: string | null;
}

export interface EnvDiagnostic {
  name: string;
  present: boolean;
  ok: boolean;
  required: boolean;
  value: string | null;
  status: "ok" | "missing" | "mismatch";
}

export interface FileDiagnostic {
  path: string;
  exists: boolean;
  ok: boolean;
  required: boolean;
  kind: "file" | "directory" | "other" | "missing";
  expectedKind: "file" | "directory" | "any" | null;
  len: number | null;
}

export interface DiagnosticMessage {
  level: "info" | "warning" | "error";
  message: string;
}

export interface ToolingService {
  diagnose(request: ToolchainDiagnosticRequest): Promise<ToolchainDiagnostic>;
}

export type PublicEvent = PluginPublicEvent;
export type PublicEventType = PluginPublicEventType;

export interface EventSubscribeOptions {
  projectId?: string;
  eventTypes?: PublicEventType[];
  capacity?: number;
  pollIntervalMs?: number;
  maxEventsPerPoll?: number;
  onDropped?: (count: number) => void;
}

export interface EventSubscription {
  readonly subscriptionId: string;
  readonly projectId: string;
  readonly eventTypes: PublicEventType[];
  readonly retention: string;
  dispose(): void;
}

export type EventHandler = (event: PublicEvent) => void;

export interface EventService {
  subscribe(options: EventSubscribeOptions, handler: EventHandler): Promise<EventSubscription>;
}

export type ConfigDocumentFormat = "json";
export type ConfigUpdateMode = "mergePatch" | "replace";

export interface ConfigDocumentReadOptions {
  projectId?: string;
  path: string;
  format?: ConfigDocumentFormat;
}

export interface ConfigDocumentUpdateOptions extends ConfigDocumentReadOptions {
  mode?: ConfigUpdateMode;
  value: JsonValue;
}

export interface ConfigDocument<T extends JsonValue = JsonValue> {
  projectId: string;
  path: string;
  format: ConfigDocumentFormat;
  value: T;
}

export interface ConfigDocumentWriteResult {
  projectId: string;
  path: string;
  format: ConfigDocumentFormat;
  mode: ConfigUpdateMode;
  bytesWritten: number;
}

export interface ConfigDocumentService {
  readDocument<T extends JsonValue = JsonValue>(
    options: ConfigDocumentReadOptions,
  ): Promise<ConfigDocument<T>>;
  updateDocument(options: ConfigDocumentUpdateOptions): Promise<ConfigDocumentWriteResult>;
}

export interface BackgroundTaskService {
  runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>;
  getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
  list(projectId?: string): Promise<BackgroundTaskStatus[]>;
  getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
  attachOutput(
    taskId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<BackgroundTaskOutputAttachment>;
  cancel(taskId: string): Promise<void>;
  retry(taskId: string): Promise<BackgroundTaskRetryResult>;
}

export interface TerminalOpenOptions {
  cwd?: string;
  rows?: number;
  cols?: number;
  onData?: (bytes: Uint8Array) => void;
}

export interface TerminalReattachOptions {
  onData?: (bytes: Uint8Array) => void;
}

export interface TerminalSession {
  readonly sessionId: string;
  write(data: Uint8Array): Promise<void>;
  resize(rows: number, cols: number): Promise<void>;
  detach(): void;
  close(): Promise<void>;
}

export interface TerminalReattachResult {
  session: TerminalSession;
  scrollback: Uint8Array;
}

export interface TerminalService {
  open(options?: TerminalOpenOptions): Promise<TerminalSession>;
  reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
  close(sessionId: string): Promise<void>;
}

export interface OpenPluginWindowOptions {
  layoutType: string;
  state?: JsonValue;
}

export interface PluginWindow {
  label: string;
  url: string;
  alreadyOpen: boolean;
  providerPluginDisplayName: string;
  layoutLabel: string;
  surface: {
    pluginId: string;
    layoutType: string;
    state: JsonValue;
  };
}

export interface WindowService {
  open(options: OpenPluginWindowOptions): Promise<PluginWindow>;
}

interface PluginServiceGatewaySet {
  project: ProjectGateway;
  terminal: TerminalGateway;
  window: WindowGateway;
  workState: WorkStateGateway;
  focusedProject: FocusedProjectGateway;
  pluginWorkspace: PluginWorkspaceGateway;
  pluginTask: PluginTaskGateway;
  pluginToolchain: PluginToolchainGateway;
  pluginEvents: PluginEventGateway;
  pluginConfig: PluginConfigGateway;
}

export interface PluginServiceScope {
  pluginId?: string;
  declaredLayoutTypes?: ReadonlySet<string>;
}

const DEFAULT_ROWS = 24;
const DEFAULT_COLS = 80;
const DEFAULT_EVENT_POLL_INTERVAL_MS = 1000;
const MIN_EVENT_POLL_INTERVAL_MS = 100;

function noopDataHandler(): void {
  // Intentionally empty: plugin code may opt into output bytes per call.
}

function toWorkspaceProject(project: FocusedProject): WorkspaceProject {
  return { id: project.id, name: project.name, root: project.root };
}

function toBackgroundTaskStatus(task: BackgroundCompletion): BackgroundTaskStatus {
  return {
    taskId: task.taskId,
    ownerAgentId: task.ownerAgentId,
    projectId: task.projectId,
    kind: task.kind,
    status: task.status,
    exitCode: task.exitCode,
    summary: task.summary,
    stdoutTail: task.stdoutTail,
    stderrTail: task.stderrTail,
    updatedAtMs: task.updatedAtMs,
  };
}

function toCommandTaskStatus(task: PluginCommandTask): CommandTaskStatus {
  return {
    taskId: task.taskId,
    ownerAgentId: task.ownerAgentId,
    projectId: task.projectId,
    kind: task.kind,
    state: task.state,
    exitCode: task.exitCode,
    summary: task.summary,
    stdoutTail: task.stdoutTail,
    stderrTail: task.stderrTail,
    createdAtMs: task.createdAtMs,
    updatedAtMs: task.updatedAtMs,
  };
}

function envEntries(env: RunCommandTaskOptions["env"]): Array<[string, string]> {
  if (!env) return [];
  return Array.isArray(env) ? env : Object.entries(env);
}

function toolEnvEntries(env: ToolRequirement["env"]): Array<[string, string]> {
  if (!env) return [];
  return Array.isArray(env) ? env : Object.entries(env);
}

function commandLabel(options: RunCommandTaskOptions): string {
  if (options.label?.trim()) return options.label;
  return [options.command, ...(options.args ?? [])].join(" ");
}

function eventPollIntervalMs(options: EventSubscribeOptions): number {
  return Math.max(options.pollIntervalMs ?? DEFAULT_EVENT_POLL_INTERVAL_MS, MIN_EVENT_POLL_INTERVAL_MS);
}

function workspaceWatchKind(operation: string): WorkspaceWatchEvent["kind"] {
  const normalized = operation.toLowerCase();
  if (normalized.includes("create") || normalized.includes("write")) return "created";
  if (normalized.includes("delete") || normalized.includes("remove")) return "deleted";
  if (normalized.includes("rename") || normalized.includes("move")) return "renamed";
  if (normalized.includes("modify") || normalized.includes("update")) return "modified";
  return "unknown";
}

function workspacePathMatches(watchedPath: string, eventPath: string): boolean {
  return (
    watchedPath === "" ||
    eventPath === watchedPath ||
    eventPath.startsWith(`${watchedPath}/`)
  );
}

export function createPluginServices(
  gateways: PluginServiceGatewaySet,
  scope: PluginServiceScope = {},
): PluginServices {
  async function currentProject(): Promise<WorkspaceProject | null> {
    const focused = await gateways.focusedProject.getFocusedProject();
    return focused ? toWorkspaceProject(focused) : null;
  }

  async function requireProject(projectId?: string): Promise<WorkspaceProject> {
    if (projectId) {
      const project = (await gateways.project.listProjects()).find((p) => p.id === projectId);
      if (!project) throw new Error(`project not found: ${projectId}`);
      return { id: project.id, name: project.name, root: project.root };
    }

    const focused = await currentProject();
    if (!focused) throw new Error("no current project is focused");
    return focused;
  }

  async function subscribeToEvents(
    options: EventSubscribeOptions,
    handler: EventHandler,
  ): Promise<EventSubscription> {
    const project = await requireProject(options.projectId);
    const subscription = await gateways.pluginEvents.subscribe({
      projectId: project.id,
      eventTypes: options.eventTypes ?? [],
      capacity: options.capacity,
    });
    let disposed = false;
    let polling = false;
    const poll = async () => {
      if (disposed || polling) return;
      polling = true;
      try {
        const batch = await gateways.pluginEvents.poll({
          subscriptionId: subscription.subscriptionId,
          maxEvents: options.maxEventsPerPoll,
        });
        if (disposed) return;
        if (batch.dropped > 0) options.onDropped?.(batch.dropped);
        for (const event of batch.events) {
          handler(event);
        }
      } catch (error) {
        if (!disposed) console.warn("[plugin-events] poll failed", error);
      } finally {
        polling = false;
      }
    };
    void poll();
    const timer = setInterval(() => void poll(), eventPollIntervalMs(options));
    return {
      subscriptionId: subscription.subscriptionId,
      projectId: subscription.projectId,
      eventTypes: subscription.eventTypes,
      retention: subscription.retention,
      dispose() {
        if (disposed) return;
        disposed = true;
        clearInterval(timer);
        void gateways.pluginEvents.unsubscribe({
          subscriptionId: subscription.subscriptionId,
        });
      },
    };
  }

  const workspace: WorkspaceService = {
    getCurrentProject: currentProject,
    async getProjectRoot(projectId) {
      return (await requireProject(projectId)).root;
    },
    async readProjectContext(projectId) {
      const project = await requireProject(projectId);
      return gateways.project.readProjectContext(project.id);
    },
    async updateProjectContext(content, projectId) {
      const project = await requireProject(projectId);
      await gateways.project.updateProjectContext(project.id, content);
    },
    async resolvePath(path, projectId) {
      const project = await requireProject(projectId);
      const stat = await gateways.pluginWorkspace.stat({ projectId: project.id, path });
      return { projectId: project.id, root: project.root, path: stat.path };
    },
    async readTextFile(path, projectId) {
      const project = await requireProject(projectId);
      return gateways.pluginWorkspace.readText({ projectId: project.id, path });
    },
    async readBinaryFile(path, projectId) {
      const project = await requireProject(projectId);
      return gateways.pluginWorkspace.readBinary({ projectId: project.id, path });
    },
    async writeTextFile(path, content, projectId) {
      const project = await requireProject(projectId);
      await gateways.pluginWorkspace.writeText({ projectId: project.id, path, content });
    },
    async writeBinaryFile(path, bytes, projectId) {
      const project = await requireProject(projectId);
      await gateways.pluginWorkspace.writeBinary({ projectId: project.id, path, bytes });
    },
    async listDirectory(path = ".", projectId) {
      const project = await requireProject(projectId);
      return gateways.pluginWorkspace.listDir({ projectId: project.id, path });
    },
    async stat(path, projectId) {
      const project = await requireProject(projectId);
      return gateways.pluginWorkspace.stat({ projectId: project.id, path });
    },
    async watch(path, handler, projectId) {
      const project = await requireProject(projectId);
      const stat = await gateways.pluginWorkspace.stat({ projectId: project.id, path });
      const subscription = await subscribeToEvents(
        { projectId: project.id, eventTypes: ["workspaceFileChanged"] },
        (event) => {
          if (event.type !== "workspaceFileChanged") return;
          if (!workspacePathMatches(stat.path, event.path)) return;
          handler({
            path: event.path,
            kind: workspaceWatchKind(event.operation),
            operation: event.operation,
            projectId: event.projectId,
          });
        },
      );
      return { dispose: () => subscription.dispose() };
    },
    async queryStructure(query = {}) {
      const project = await requireProject(query.projectId);
      const input: PluginProjectStructureQuery = {
        projectId: project.id,
        path: query.path,
        maxDepth: query.maxDepth,
        maxEntries: query.maxEntries,
      };
      return gateways.pluginWorkspace.queryProjectStructure(input);
    },
  };

  async function listTasks(projectId?: string): Promise<BackgroundTaskStatus[]> {
    const project = await requireProject(projectId);
    const state = await gateways.workState.getProjectWorkState(project.id);
    return state.agents
      .flatMap((agent) => agent.backgroundTasks ?? [])
      .map(toBackgroundTaskStatus);
  }

  const tasks: BackgroundTaskService = {
    async runCommand(options) {
      const project = await requireProject(options.projectId);
      if (options.ownerAgentId.trim() === "") {
        throw new Error("ownerAgentId is required to correlate a plugin command task");
      }
      const task = await gateways.pluginTask.runCommand({
        projectId: project.id,
        ownerAgentId: options.ownerAgentId,
        label: commandLabel(options),
        command: options.command,
        args: options.args ?? [],
        cwd: options.cwd,
        env: envEntries(options.env),
        recordOnly: options.recordOnly ?? false,
        deadlineMs: options.deadlineMs,
      });
      return toCommandTaskStatus(task);
    },
    async getCommandStatus(taskId) {
      const task = await gateways.pluginTask.getStatus({ taskId });
      return task ? toCommandTaskStatus(task) : null;
    },
    list: listTasks,
    async getStatus(taskId, projectId) {
      return (await listTasks(projectId)).find((task) => task.taskId === taskId) ?? null;
    },
    async attachOutput(taskId, onData) {
      const attachment: BackgroundTaskAttachment =
        await gateways.workState.attachBackgroundTask(taskId, onData);
      return {
        taskId: attachment.taskId,
        scrollback: attachment.scrollback,
        live: attachment.live,
        detach: () => attachment.detach(),
      };
    },
    async cancel(taskId) {
      await gateways.workState.cancelBackgroundTask(taskId);
    },
    async retry(taskId) {
      await gateways.workState.retryBackgroundTask(taskId);
      return {};
    },
  };

  const tooling: ToolingService = {
    async diagnose(request) {
      const project = await requireProject(request.projectId);
      return gateways.pluginToolchain.diagnose({
        projectId: project.id,
        cwd: request.cwd,
        tools: (request.tools ?? []).map((tool) => ({
          id: tool.id,
          executable: tool.executable,
          versionArgs: tool.versionArgs ?? [],
          required: tool.required ?? false,
          env: toolEnvEntries(tool.env),
        })),
        env: request.env ?? [],
        files: request.files ?? [],
      });
    },
  };

  const events: EventService = {
    subscribe: subscribeToEvents,
  };

  const config: ConfigDocumentService = {
    async readDocument<T extends JsonValue = JsonValue>(
      options: ConfigDocumentReadOptions,
    ): Promise<ConfigDocument<T>> {
      const project = await requireProject(options.projectId);
      const document = await gateways.pluginConfig.readDocument({
        projectId: project.id,
        path: options.path,
        format: options.format,
      });
      return document as ConfigDocument<T>;
    },
    async updateDocument(options) {
      const project = await requireProject(options.projectId);
      return gateways.pluginConfig.updateDocument({
        projectId: project.id,
        path: options.path,
        format: options.format,
        mode: options.mode,
        value: options.value,
      });
    },
  };

  const terminal: TerminalService = {
    async open(options = {}) {
      const cwd = options.cwd ?? (await workspace.getProjectRoot());
      const openOptions: OpenTerminalOptions = {
        cwd,
        rows: options.rows ?? DEFAULT_ROWS,
        cols: options.cols ?? DEFAULT_COLS,
      };
      return gateways.terminal.openTerminal(openOptions, options.onData ?? noopDataHandler);
    },
    async reattach(sessionId, options = {}) {
      const result = await gateways.terminal.reattach(
        sessionId,
        options.onData ?? noopDataHandler,
      );
      return { session: result.handle, scrollback: result.scrollback };
    },
    async close(sessionId) {
      await gateways.terminal.closeTerminal(sessionId);
    },
  };

  const windows: WindowService = {
    async open(options) {
      const layoutType = options.layoutType.trim();
      if (!layoutType) {
        throw new Error("layoutType is required");
      }
      if (scope.declaredLayoutTypes && !scope.declaredLayoutTypes.has(layoutType)) {
        throw new Error(
          `plugin "${scope.pluginId ?? "<unknown>"}" cannot open undeclared layout "${layoutType}"`,
        );
      }
      const pluginId = scope.pluginId;
      if (!pluginId) {
        throw new Error("plugin id is required to open a plugin layout window");
      }
      return gateways.window.openPluginLayoutWindow({
        pluginId,
        layoutType,
        state: options.state ?? null,
      });
    },
  };

  return { workspace, tasks, tooling, events, config, terminal, windows };
}
