import { execFile } from 'child_process';
import fs from 'fs';
import path from 'path';

import {
  ARBITER_AGENT_TYPE,
  ARBITER_MODEL_CONFIG,
  ARBITER_SERVICE_ID,
  CLAUDE_SERVICE_ID,
  CODEX_MAIN_SERVICE_ID,
  CODEX_REVIEW_SERVICE_ID,
  CURRENT_RUNTIME_AGENT_TYPE,
  DEFAULT_CLAUDE_MODEL,
  DEFAULT_CODEX_MODEL,
  OWNER_AGENT_TYPE,
  OWNER_MODEL_CONFIG,
  REVIEWER_AGENT_TYPE,
  REVIEWER_MODEL_CONFIG,
  REVIEWER_SERVICE_ID_FOR_TYPE,
  SERVICE_ID,
  SERVICE_SESSION_SCOPE,
  DATA_DIR,
  WEB_DASHBOARD,
} from './config.js';
import type { AgentType } from './types.js';
import {
  HOST_EVIDENCE_ACTIONS,
  isHostEvidenceAction,
  type HostEvidenceAction,
} from 'ejclaw-runners-shared';
import {
  collectArtifactMetadata,
  collectDeployState,
} from './deploy-evidence.js';
import { isDbEvidenceAction, runDbEvidenceRequest } from './db-evidence.js';
import { requireDatabase } from './db/runtime-database.js';
import {
  isGitHubEvidenceAction,
  runGitHubEvidenceCommand,
} from './github-evidence.js';
import { resolveGroupIpcPath } from './group-folder.js';

export { HOST_EVIDENCE_ACTIONS, isHostEvidenceAction, type HostEvidenceAction };

export interface HostEvidenceRequest {
  requestId: string;
  action: HostEvidenceAction;
  tailLines?: number;
  taskId?: string;
  minutes?: number;
  limit?: number;
  repo?: string;
  prNumber?: number;
  runId?: number;
  workflowPath?: string;
  ref?: string;
  artifactKind?: string;
  sourceGroup?: string;
  isMain?: boolean;
}

export interface HostEvidenceResult {
  ok: boolean;
  action: HostEvidenceAction | 'ejclaw_room_runtime';
  command: string;
  stdout: string;
  stderr: string;
  exitCode: number;
  error?: string;
}

export interface HostEvidenceResponse extends HostEvidenceResult {
  requestId: string;
}

interface HostEvidenceCommandSpec {
  file: string;
  args: string[];
  commandText: string;
}

const DEFAULT_LOG_TAIL_LINES = 20;
const MAX_LOG_TAIL_LINES = 200;
const MAX_OUTPUT_CHARS = 16_000;
const COMMAND_TIMEOUT_MS = 5_000;
const COMMAND_MAX_BUFFER = 1024 * 1024;
const PROJECT_ROOT = process.cwd();

export function clampHostEvidenceTailLines(value?: number): number {
  if (!Number.isFinite(value)) {
    return DEFAULT_LOG_TAIL_LINES;
  }
  const normalized = Math.trunc(value as number);
  return Math.min(Math.max(normalized, 1), MAX_LOG_TAIL_LINES);
}

export function buildHostEvidenceCommand(
  request: HostEvidenceRequest,
): HostEvidenceCommandSpec {
  switch (request.action) {
    case 'ejclaw_service_status': {
      const args = [
        '--user',
        'show',
        'ejclaw',
        '-p',
        'Id',
        '-p',
        'LoadState',
        '-p',
        'ActiveState',
        '-p',
        'SubState',
        '-p',
        'ExecMainCode',
        '-p',
        'ExecMainStatus',
        '-p',
        'ExecMainStartTimestamp',
        '-p',
        'ActiveEnterTimestamp',
      ];
      return {
        file: 'systemctl',
        args,
        commandText: `systemctl ${args.join(' ')}`,
      };
    }
    case 'ejclaw_service_logs': {
      const tailLines = clampHostEvidenceTailLines(request.tailLines);
      const args = [
        '--user',
        '-u',
        'ejclaw',
        '--no-pager',
        '-n',
        String(tailLines),
      ];
      return {
        file: 'journalctl',
        args,
        commandText: `journalctl ${args.join(' ')}`,
      };
    }
    default:
      throw new Error(
        `Host evidence action has no shell command: ${request.action}`,
      );
  }
}

export function truncateHostEvidenceText(value: string | undefined): string {
  if (!value) return '';
  if (value.length <= MAX_OUTPUT_CHARS) {
    return value;
  }
  return `${value.slice(0, MAX_OUTPUT_CHARS)}\n...[truncated]`;
}

function effectiveModelForAgentType(
  agentType: AgentType | null | undefined,
  configuredModel: string | undefined,
): string | null {
  if (!agentType) {
    return null;
  }
  if (configuredModel) {
    return configuredModel;
  }
  return agentType === 'claude-code'
    ? DEFAULT_CLAUDE_MODEL
    : DEFAULT_CODEX_MODEL;
}

function ownerServiceIdForAgentType(agentType: AgentType): string {
  return agentType === 'claude-code'
    ? CLAUDE_SERVICE_ID
    : CODEX_MAIN_SERVICE_ID;
}

function buildRoleRuntimeConfigRole(
  role: 'owner' | 'reviewer' | 'arbiter',
  agentType: AgentType | null | undefined,
  modelConfig: {
    model?: string;
    effort?: string;
    fallbackEnabled: boolean;
  },
  serviceId: string | null,
): Record<string, unknown> {
  return {
    role,
    enabled: role !== 'arbiter' || Boolean(agentType),
    agent_type: agentType ?? null,
    service_id: serviceId,
    configured_model: modelConfig.model ?? null,
    effective_model: effectiveModelForAgentType(agentType, modelConfig.model),
    effort: modelConfig.effort ?? null,
    fallback_enabled: modelConfig.fallbackEnabled,
  };
}

export function buildRoleRuntimeConfigEvidence(): string {
  return JSON.stringify(
    {
      action: 'ejclaw_role_runtime_config',
      current_service: {
        service_id: SERVICE_ID,
        session_scope: SERVICE_SESSION_SCOPE,
        runtime_agent_type: CURRENT_RUNTIME_AGENT_TYPE,
      },
      defaults: {
        claude_model: DEFAULT_CLAUDE_MODEL,
        codex_model: DEFAULT_CODEX_MODEL,
      },
      services: {
        claude: CLAUDE_SERVICE_ID,
        codex_main: CODEX_MAIN_SERVICE_ID,
        codex_review: CODEX_REVIEW_SERVICE_ID,
        reviewer_for_type: REVIEWER_SERVICE_ID_FOR_TYPE,
        arbiter: ARBITER_SERVICE_ID,
      },
      roles: {
        owner: buildRoleRuntimeConfigRole(
          'owner',
          OWNER_AGENT_TYPE,
          OWNER_MODEL_CONFIG,
          ownerServiceIdForAgentType(OWNER_AGENT_TYPE),
        ),
        reviewer: buildRoleRuntimeConfigRole(
          'reviewer',
          REVIEWER_AGENT_TYPE,
          REVIEWER_MODEL_CONFIG,
          REVIEWER_SERVICE_ID_FOR_TYPE,
        ),
        arbiter: buildRoleRuntimeConfigRole(
          'arbiter',
          ARBITER_AGENT_TYPE,
          ARBITER_MODEL_CONFIG,
          ARBITER_SERVICE_ID,
        ),
      },
    },
    null,
    2,
  );
}

function execFileCapture(
  file: string,
  args: string[],
): Promise<{ stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    execFile(
      file,
      args,
      {
        encoding: 'utf8',
        timeout: COMMAND_TIMEOUT_MS,
        maxBuffer: COMMAND_MAX_BUFFER,
      },
      (error, stdout, stderr) => {
        if (error) {
          reject(
            Object.assign(error, {
              stdout,
              stderr,
            }),
          );
          return;
        }
        resolve({
          stdout,
          stderr,
        });
      },
    );
  });
}

function extractExitCode(error: unknown): number {
  if (typeof error === 'object' && error !== null && 'code' in error) {
    const code = (error as { code?: unknown }).code;
    if (typeof code === 'number') {
      return code;
    }
  }
  return 1;
}

export async function runHostEvidenceRequest(
  request: HostEvidenceRequest,
): Promise<HostEvidenceResult> {
  let commandText = '';
  try {
    if (request.action === 'ejclaw_role_runtime_config') {
      commandText = `internal:${request.action}`;
      return {
        ok: true,
        action: request.action,
        command: commandText,
        stdout: truncateHostEvidenceText(buildRoleRuntimeConfigEvidence()),
        stderr: '',
        exitCode: 0,
      };
    }

    if (isDbEvidenceAction(request.action)) {
      commandText = `internal:${request.action}`;
      return {
        ok: true,
        action: request.action,
        command: commandText,
        stdout: truncateHostEvidenceText(
          runDbEvidenceRequest(
            requireDatabase(),
            {
              action: request.action,
              taskId: request.taskId,
              minutes: request.minutes,
              limit: request.limit,
            },
            {
              sourceGroup: request.sourceGroup ?? '',
              isMain: request.isMain === true,
            },
          ),
        ),
        stderr: '',
        exitCode: 0,
      };
    }

    if (request.action === 'ejclaw_deploy_state') {
      commandText = `internal:${request.action}`;
      return {
        ok: true,
        action: request.action,
        command: commandText,
        stdout: truncateHostEvidenceText(
          await collectDeployState({
            projectRoot: PROJECT_ROOT,
            dataDir: DATA_DIR,
            dashboardStaticDir: WEB_DASHBOARD.staticDir,
          }),
        ),
        stderr: '',
        exitCode: 0,
      };
    }

    if (request.action === 'ejclaw_artifact_metadata') {
      commandText = `internal:${request.action}`;
      return {
        ok: true,
        action: request.action,
        command: commandText,
        stdout: truncateHostEvidenceText(
          collectArtifactMetadata(
            {
              projectRoot: PROJECT_ROOT,
              dataDir: DATA_DIR,
              dashboardStaticDir: WEB_DASHBOARD.staticDir,
            },
            {
              action: request.action,
              artifactKind: request.artifactKind,
            },
          ),
        ),
        stderr: '',
        exitCode: 0,
      };
    }

    if (isGitHubEvidenceAction(request.action)) {
      const githubResult = await runGitHubEvidenceCommand({
        action: request.action,
        repo: request.repo,
        prNumber: request.prNumber,
        runId: request.runId,
        workflowPath: request.workflowPath,
        ref: request.ref,
      });
      commandText = githubResult.command;
      return {
        ok: true,
        action: request.action,
        command: commandText,
        stdout: truncateHostEvidenceText(githubResult.stdout),
        stderr: truncateHostEvidenceText(githubResult.stderr),
        exitCode: 0,
      };
    }

    const command = buildHostEvidenceCommand(request);
    commandText = command.commandText;
    const { stdout, stderr } = await execFileCapture(
      command.file,
      command.args,
    );
    return {
      ok: true,
      action: request.action,
      command: commandText,
      stdout: truncateHostEvidenceText(stdout),
      stderr: truncateHostEvidenceText(stderr),
      exitCode: 0,
    };
  } catch (error) {
    const stdout =
      typeof error === 'object' && error !== null && 'stdout' in error
        ? String((error as { stdout?: unknown }).stdout ?? '')
        : '';
    const stderr =
      typeof error === 'object' && error !== null && 'stderr' in error
        ? String((error as { stderr?: unknown }).stderr ?? '')
        : '';

    return {
      ok: false,
      action: request.action,
      command:
        typeof error === 'object' && error !== null && 'command' in error
          ? String((error as { command?: unknown }).command ?? commandText)
          : commandText,
      stdout: truncateHostEvidenceText(stdout),
      stderr: truncateHostEvidenceText(stderr),
      exitCode: extractExitCode(error),
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

export function resolveHostEvidenceResponseDir(groupFolder: string): string {
  return path.join(resolveGroupIpcPath(groupFolder), 'host-evidence-responses');
}

export function writeHostEvidenceResponse(
  groupFolder: string,
  response: HostEvidenceResponse,
): string {
  const responseDir = resolveHostEvidenceResponseDir(groupFolder);
  fs.mkdirSync(responseDir, { recursive: true });

  const outputPath = path.join(responseDir, `${response.requestId}.json`);
  const tempPath = `${outputPath}.tmp`;
  fs.writeFileSync(tempPath, JSON.stringify(response, null, 2));
  fs.renameSync(tempPath, outputPath);
  return outputPath;
}
