import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
import fs from "node:fs/promises";
import path from "node:path";
import { startPlanningWorkflow } from "./plan";

const ARCHITECT_OUTPUT_ROOT_SEGMENTS = ["docyrus", "architecture"] as const;
const ARCHITECT_DISCOVERY_FILE_NAME = "discovery.snapshot.json";
const ARCHITECT_DATA_SOURCES_FILE_NAME = "DATA_SOURCES.md";
const ARCHITECT_PLAN_FILE_NAME = "PLAN.md";
const ARCHITECT_PLAN_JSON_FILE_NAME = "data-sources.plan.json";
const ARCHITECT_BULK_CREATE_FILE_NAME = "data-sources.bulk-create.json";
const ARCHITECT_DISCOVERY_CONCURRENCY = 4;
const DOCYRUS_SCOPE_VALUES = new Set(["local", "global"]);
const ENUM_FIELD_TYPES = new Set([
  "field-select",
  "field-status",
  "field-multiSelect",
  "field-tagSelect",
  "field-radioGroup",
]);

export interface IArchitectCliEnvironment {
	executable: string;
	entryPath: string;
	scope: "local" | "global";
}

export interface IArchitectCliCommandSuccess {
	ok: true;
	command: string;
	payload: unknown;
	messages: string[];
}

export interface IArchitectCliCommandFailure {
	ok: false;
	command: string;
	error: string;
	details?: string;
}

export type IArchitectCliCommandResult =
	| IArchitectCliCommandSuccess
	| IArchitectCliCommandFailure;

export interface IArchitectCliEnvelope {
	data: unknown;
	context: Record<string, unknown> | null;
}

export interface IArchitectDiscoveryWarning {
	code: string;
	message: string;
	command?: string;
	appId?: string;
	appSlug?: string;
	dataSourceId?: string;
	dataSourceSlug?: string;
	fieldId?: string;
	fieldSlug?: string;
}

export interface IArchitectFieldSnapshot {
	field: Record<string, unknown>;
	enums: Record<string, unknown>[];
}

export interface IArchitectDataSourceSnapshot {
	dataSource: Record<string, unknown>;
	fields: IArchitectFieldSnapshot[];
}

export interface IArchitectAppSnapshot {
	app: Record<string, unknown>;
	dataSources: IArchitectDataSourceSnapshot[];
}

export interface IArchitectDiscoverySnapshot {
	version: 1;
	generatedAt: string;
	tenantContext: Record<string, unknown> | null;
	authenticatedUser: Record<string, unknown> | null;
	apps: IArchitectAppSnapshot[];
	warnings: IArchitectDiscoveryWarning[];
	summary: {
		appCount: number;
		dataSourceCount: number;
		fieldCount: number;
		enumFieldCount: number;
		enumOptionCount: number;
	};
}

export interface IArchitectDiscoverySuccess {
	ok: true;
	snapshot: IArchitectDiscoverySnapshot;
}

export interface IArchitectDiscoveryFailure {
	ok: false;
	error: string;
	command?: string;
	details?: string;
}

export type IArchitectDiscoveryResult =
	| IArchitectDiscoverySuccess
	| IArchitectDiscoveryFailure;

export type IArchitectCliRunner = (args: string[]) => Promise<IArchitectCliCommandResult>;

interface IParsedCliJsonOutput {
	payload: unknown;
	messages: string[];
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function extractString(record: Record<string, unknown>, key: string): string | undefined {
  const value = record[key];
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}

function omitKeys(record: Record<string, unknown>, keys: string[]): Record<string, unknown> {
  const omitted = new Set(keys);
  return Object.fromEntries(Object.entries(record).filter(([key]) => !omitted.has(key)));
}

function toRecordArray(value: unknown): Record<string, unknown>[] {
  if (Array.isArray(value)) {
    return value.filter((item): item is Record<string, unknown> => isRecord(item));
  }

  if (isRecord(value)) {
    if (Array.isArray(value.data)) {
      return value.data.filter((item): item is Record<string, unknown> => isRecord(item));
    }

    if (Array.isArray(value.items)) {
      return value.items.filter((item): item is Record<string, unknown> => isRecord(item));
    }
  }

  return [];
}

function sanitizeCliArgs(args: string[]): string[] {
  return args
    .map((value) => value.trim())
    .filter((value) => value.length > 0)
    .filter((value) => value !== "--json");
}

export function parseArchitectBrief(rawArgs: string): string | null {
  const normalized = rawArgs.trim();
  return normalized.length > 0 ? normalized : null;
}

export function slugifyArchitectBrief(brief: string): string {
  const slug = brief
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 60);

  return slug || "app-idea";
}

export function formatArchitectTimestamp(date: Date): string {
  const year = `${date.getFullYear()}`;
  const month = `${date.getMonth() + 1}`.padStart(2, "0");
  const day = `${date.getDate()}`.padStart(2, "0");
  const hour = `${date.getHours()}`.padStart(2, "0");
  const minute = `${date.getMinutes()}`.padStart(2, "0");
  const second = `${date.getSeconds()}`.padStart(2, "0");
  return `${year}${month}${day}-${hour}${minute}${second}`;
}

export function createArchitectRunDirectoryPath(params: {
	cwd: string;
	brief: string;
	date?: Date;
}): string {
  const timestamp = formatArchitectTimestamp(params.date ?? new Date());
  const slug = slugifyArchitectBrief(params.brief);
  return path.join(params.cwd, ...ARCHITECT_OUTPUT_ROOT_SEGMENTS, `${timestamp}-${slug}`);
}

export function normalizeArchitectCliEnvelope(payload: unknown): IArchitectCliEnvelope {
  if (isRecord(payload) && Object.hasOwn(payload, "data")) {
    return {
      data: payload.data,
      context: isRecord(payload.context) ? payload.context : null,
    };
  }

  return {
    data: payload,
    context: isRecord(payload) && isRecord(payload.context) ? payload.context : null,
  };
}

export function parseArchitectCliJsonOutput(rawOutput: string): IParsedCliJsonOutput {
  const trimmedOutput = rawOutput.trim();
  if (!trimmedOutput) {
    throw new Error("Command produced no output.");
  }

  const lines = trimmedOutput
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter((line) => line.length > 0);

  const lastLine = lines.at(-1);
  if (lastLine) {
    try {
      return {
        payload: JSON.parse(lastLine) as unknown,
        messages: lines.slice(0, -1),
      };
    }
    catch {
			// Fall back to parsing the whole output.
    }
  }

  return {
    payload: JSON.parse(trimmedOutput) as unknown,
    messages: [],
  };
}

export function appendArchitectWarning(
  warnings: IArchitectDiscoveryWarning[],
  warning: IArchitectDiscoveryWarning,
): IArchitectDiscoveryWarning[] {
  const key = JSON.stringify(warning);
  const seen = new Set(warnings.map((item) => JSON.stringify(item)));
  if (seen.has(key)) {
    return warnings;
  }

  return [...warnings, warning];
}

function buildDocyrusCliCommand(args: string[], scope: "local" | "global"): string[] {
  const sanitizedArgs = sanitizeCliArgs(args);
  const scopedArgs = scope === "global" ? ["-g", ...sanitizedArgs] : sanitizedArgs;
  return [...scopedArgs, "--json"];
}

function formatDocyrusCommandLabel(args: string[], scope: "local" | "global"): string {
  const parts = ["docyrus", ...(scope === "global" ? ["-g"] : []), ...sanitizeCliArgs(args), "--json"];
  return parts.join(" ");
}

function summarizeCliFailure(command: string, stderr: string, stdout: string, code: number | null | undefined): string {
  const trimmedStderr = stderr.trim();
  if (trimmedStderr) {
    return trimmedStderr;
  }

  const trimmedStdout = stdout.trim();
  if (trimmedStdout) {
    return trimmedStdout.split(/\r?\n/).at(-1) || trimmedStdout;
  }

  if (typeof code === "number") {
    return `Command exited with code ${code}.`;
  }

  return `Command failed: ${command}`;
}

function readArchitectCliEnvironment(env: NodeJS.ProcessEnv = process.env): IArchitectCliEnvironment {
  const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
  const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
  const scope = env.DOCYRUS_CLI_SCOPE?.trim() as "local" | "global" | undefined;

  if (!executable || !entryPath || !scope || !DOCYRUS_SCOPE_VALUES.has(scope)) {
    throw new Error(
      "Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.",
    );
  }

  return {
    executable,
    entryPath,
    scope,
  };
}

function shouldDiscoverEnums(field: Record<string, unknown>): boolean {
  const type = extractString(field, "type");
  return typeof type === "string" && ENUM_FIELD_TYPES.has(type);
}

async function mapWithConcurrency<TInput, TOutput>(
  items: TInput[],
  limit: number,
  mapper: (item: TInput, index: number) => Promise<TOutput>,
): Promise<TOutput[]> {
  if (items.length === 0) {
    return [];
  }

  const safeLimit = Math.max(1, Math.min(limit, items.length));
  const results = new Array<TOutput>(items.length);
  let nextIndex = 0;

  await Promise.all(
    Array.from({ length: safeLimit }, async() => {
      while (nextIndex < items.length) {
        const currentIndex = nextIndex;
        nextIndex += 1;
        results[currentIndex] = await mapper(items[currentIndex], currentIndex);
      }
    }),
  );

  return results;
}

export async function discoverArchitectTenantState(params: {
	runCliCommand: IArchitectCliRunner;
	generatedAt?: string;
	maxConcurrency?: number;
}): Promise<IArchitectDiscoveryResult> {
  const runCliCommand = params.runCliCommand;
  const generatedAt = params.generatedAt ?? new Date().toISOString();
  const maxConcurrency = params.maxConcurrency ?? ARCHITECT_DISCOVERY_CONCURRENCY;

  const authResult = await runCliCommand(["auth", "who"]);
  if (!authResult.ok) {
    return {
      ok: false,
      command: authResult.command,
      details: authResult.details,
      error: "Architect requires an active Docyrus session. Run `docyrus auth login` first.",
    };
  }

  const authEnvelope = normalizeArchitectCliEnvelope(authResult.payload);
  const authenticatedUser = isRecord(authEnvelope.data) ? authEnvelope.data : null;
  const tenantContext = authEnvelope.context;

  const appsResult = await runCliCommand(["apps", "list"]);
  if (!appsResult.ok) {
    return {
      ok: false,
      command: appsResult.command,
      details: appsResult.details,
      error: `Failed to list tenant apps: ${appsResult.error}`,
    };
  }

  const apps = toRecordArray(normalizeArchitectCliEnvelope(appsResult.payload).data);
  const appResults = await mapWithConcurrency(apps, maxConcurrency, async(app) => {
    const appId = extractString(app, "id");
    const appSlug = extractString(app, "slug");
    let warnings: IArchitectDiscoveryWarning[] = [];

    if (!appId) {
      warnings = appendArchitectWarning(warnings, {
        code: "app_missing_id",
        message: "Skipping app because it has no id in CLI output.",
        appSlug,
      });

      return {
        app,
        dataSources: [] as IArchitectDataSourceSnapshot[],
        warnings,
      };
    }

    const dataSourcesResult = await runCliCommand([
      "studio",
      "list-data-sources",
      "--appId",
      appId,
      "--expand",
      "fields",
    ]);

    if (!dataSourcesResult.ok) {
      warnings = appendArchitectWarning(warnings, {
        code: "data_source_list_failed",
        message: dataSourcesResult.error,
        command: dataSourcesResult.command,
        appId,
        appSlug,
      });

      return {
        app,
        dataSources: [] as IArchitectDataSourceSnapshot[],
        warnings,
      };
    }

    const dataSources = toRecordArray(normalizeArchitectCliEnvelope(dataSourcesResult.payload).data);
    const dataSourceResults = await mapWithConcurrency(dataSources, maxConcurrency, async(dataSource) => {
      const dataSourceId = extractString(dataSource, "id");
      const dataSourceSlug = extractString(dataSource, "slug");
      const fields = toRecordArray(dataSource.fields);
      const fieldResults = await mapWithConcurrency(fields, maxConcurrency, async(field) => {
        let fieldWarnings: IArchitectDiscoveryWarning[] = [];

        if (!shouldDiscoverEnums(field)) {
          return {
            fieldSnapshot: {
              field,
              enums: [],
            },
            warnings: fieldWarnings,
          };
        }

        const fieldId = extractString(field, "id");
        const fieldSlug = extractString(field, "slug");
        if (!dataSourceId || !fieldId) {
          fieldWarnings = appendArchitectWarning(fieldWarnings, {
            code: "enum_lookup_skipped",
            message: "Skipping enum lookup because the field or data source id is missing.",
            appId,
            appSlug,
            dataSourceId,
            dataSourceSlug,
            fieldId,
            fieldSlug,
          });

          return {
            fieldSnapshot: {
              field,
              enums: [],
            },
            warnings: fieldWarnings,
          };
        }

        const enumsResult = await runCliCommand([
          "studio",
          "list-enums",
          "--appId",
          appId,
          "--dataSourceId",
          dataSourceId,
          "--fieldId",
          fieldId,
        ]);

        if (!enumsResult.ok) {
          fieldWarnings = appendArchitectWarning(fieldWarnings, {
            code: "enum_lookup_failed",
            message: enumsResult.error,
            command: enumsResult.command,
            appId,
            appSlug,
            dataSourceId,
            dataSourceSlug,
            fieldId,
            fieldSlug,
          });

          return {
            fieldSnapshot: {
              field,
              enums: [],
            },
            warnings: fieldWarnings,
          };
        }

        return {
          fieldSnapshot: {
            field,
            enums: toRecordArray(normalizeArchitectCliEnvelope(enumsResult.payload).data),
          },
          warnings: fieldWarnings,
        };
      });

      const fieldSnapshots = fieldResults.map((item) => item.fieldSnapshot);
      const nestedWarnings = fieldResults.flatMap((item) => item.warnings);
      return {
        dataSourceSnapshot: {
          dataSource: omitKeys(dataSource, ["fields"]),
          fields: fieldSnapshots,
        },
        warnings: nestedWarnings,
      };
    });

    return {
      app,
      dataSources: dataSourceResults.map((item) => item.dataSourceSnapshot),
      warnings: [...warnings, ...dataSourceResults.flatMap((item) => item.warnings)],
    };
  });

  const warnings = appResults.flatMap((item) => item.warnings);
  const snapshot: IArchitectDiscoverySnapshot = {
    version: 1,
    generatedAt,
    tenantContext,
    authenticatedUser,
    apps: appResults.map((item) => ({
      app: item.app,
      dataSources: item.dataSources,
    })),
    warnings,
    summary: {
      appCount: appResults.length,
      dataSourceCount: appResults.reduce((count, item) => count + item.dataSources.length, 0),
      fieldCount: appResults.reduce(
        (count, item) => count + item.dataSources.reduce((inner, dataSource) => inner + dataSource.fields.length, 0),
        0,
      ),
      enumFieldCount: appResults.reduce(
        (count, item) => count + item.dataSources.reduce(
          (inner, dataSource) => inner + dataSource.fields.filter((field) => field.enums.length > 0).length,
          0,
        ),
        0,
      ),
      enumOptionCount: appResults.reduce(
        (count, item) => count + item.dataSources.reduce(
          (inner, dataSource) => inner + dataSource.fields.reduce((fieldCount, field) => fieldCount + field.enums.length, 0),
          0,
        ),
        0,
      ),
    },
  };

  return {
    ok: true,
    snapshot,
  };
}

function buildArchitectPrompt(params: {
	brief?: string;
	outputDir: string;
	snapshot: IArchitectDiscoverySnapshot;
}): string {
  const discoveryPath = path.join(params.outputDir, ARCHITECT_DISCOVERY_FILE_NAME);
  const dataSourcesPath = path.join(params.outputDir, ARCHITECT_DATA_SOURCES_FILE_NAME);
  const planPath = path.join(params.outputDir, ARCHITECT_PLAN_FILE_NAME);
  const planJsonPath = path.join(params.outputDir, ARCHITECT_PLAN_JSON_FILE_NAME);
  const bulkCreatePath = path.join(params.outputDir, ARCHITECT_BULK_CREATE_FILE_NAME);
  const tenantDisplay = params.snapshot.tenantContext?.tenantDisplay || "Unknown tenant";
  const warningLines = params.snapshot.warnings.length > 0
    ? params.snapshot.warnings.map((warning) => `- [${warning.code}] ${warning.message}`).join("\n")
    : "- None";

  return [
    "You are running the Docyrus `/architect` workflow.",
    "",
    "This is planning only. Do not mutate remote Docyrus state.",
    "Do not run `docyrus studio` create/update/delete/bulk-create commands, `docyrus ds create/update/delete`, or any other remote mutation.",
    "Work only from the discovery snapshot plus any non-mutating local inspection you need.",
    "",
    "## User Brief",
    params.brief ?? "No brief was provided yet. Before planning, collect the minimum missing product details from the user using the ask_user protocol.",
    "",
    "## Discovery Summary",
    `- Tenant: ${tenantDisplay}`,
    `- Apps discovered: ${params.snapshot.summary.appCount}`,
    `- Data sources discovered: ${params.snapshot.summary.dataSourceCount}`,
    `- Fields discovered: ${params.snapshot.summary.fieldCount}`,
    `- Enum-bearing fields with discovered options: ${params.snapshot.summary.enumFieldCount}`,
    `- Discovery warnings: ${params.snapshot.warnings.length}`,
    "",
    "## Discovery Warnings",
    warningLines,
    "",
    "## Required Local Artifacts",
    `- Read discovery snapshot: ${discoveryPath}`,
    `- Write ${dataSourcesPath}`,
    `- Write ${planPath}`,
    `- Write ${planJsonPath}`,
    `- Optionally write ${bulkCreatePath} only if the proposed schema is directly representable by the current \`docyrus studio bulk-create-data-sources\` payload; otherwise do not create that file and explain the limitation in PLAN.md.`,
    "",
    "## DATA_SOURCES.md Requirements",
    "- Explicitly list reusable existing data sources and why each is reused or rejected.",
    "- Explicitly list every proposed new data source, required fields, enum options, and relations.",
    "- Call out unresolved assumptions and any discovery gaps. If no reusable data sources exist, say that explicitly.",
    "",
    "## PLAN.md Requirements",
    "- Provide an ordered implementation/apply plan.",
    "- Use exact `docyrus studio` workflows and commands conceptually, but do not run them.",
    "- Include verification steps.",
    "- Explain relation-field follow-up when newly proposed data sources cannot be referenced directly during bulk create because relation fields need concrete IDs.",
    "",
    "## data-sources.plan.json Contract",
    "- Include `brief`, `generatedAt`, and `tenantContext`.",
    "- Include `existingDataSources[]` with reuse decision and rationale.",
    "- Include `newDataSources[]` with `title`, `name`, `slug`, `icon`, and `fields[]`.",
    "- Each field entry must include `name`, `slug`, `type`, `validations`, and `defaultValue`.",
    "- Add optional `enumOptions[]` when applicable.",
    "- Add optional `relationTarget` as either `{ \"kind\": \"existing\", \"dataSourceId\": \"...\" }` or `{ \"kind\": \"new\", \"slug\": \"...\" }`.",
    "- Include `applyPhases[]` covering `bulkCreate`, `postCreateRelations`, `postCreateEnums`, and `verification`.",
    "",
    "## Final Response",
    "After writing the files, summarize what you wrote, note whether `data-sources.bulk-create.json` was created, and list any assumptions that still need human confirmation.",
  ].join("\n");
}

function createArchitectCliRunner(params: {
	pi: ExtensionAPI;
	ctx: ExtensionCommandContext;
	environment: IArchitectCliEnvironment;
}): IArchitectCliRunner {
  return async(args: string[]) => {
    const commandArgs = buildDocyrusCliCommand(args, params.environment.scope);
    const commandLabel = formatDocyrusCommandLabel(args, params.environment.scope);
    try {
      const result = await params.pi.exec(
        params.environment.executable,
        [params.environment.entryPath, ...commandArgs],
        { cwd: params.ctx.cwd },
      );
      const stdout = result.stdout?.toString() || "";
      const stderr = result.stderr?.toString() || "";

      if (result.code !== 0) {
        return {
          ok: false,
          command: commandLabel,
          error: summarizeCliFailure(commandLabel, stderr, stdout, result.code),
          details: [stdout.trim(), stderr.trim()].filter(Boolean).join("\n") || undefined,
        };
      }

      try {
        const parsed = parseArchitectCliJsonOutput(stdout);
        return {
          ok: true,
          command: commandLabel,
          payload: parsed.payload,
          messages: parsed.messages,
        };
      }
      catch (error) {
        return {
          ok: false,
          command: commandLabel,
          error: error instanceof Error ? error.message : String(error),
          details: stdout.trim() || undefined,
        };
      }
    }
    catch (error) {
      return {
        ok: false,
        command: commandLabel,
        error: error instanceof Error ? error.message : String(error),
      };
    }
  };
}

async function architectHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext, args: string): Promise<void> {
  const brief = parseArchitectBrief(args) ?? undefined;

  let environment: IArchitectCliEnvironment;
  try {
    environment = readArchitectCliEnvironment();
  }
  catch (error) {
    if (ctx.hasUI) {
      ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
    }
    return;
  }

  if (ctx.hasUI) {
    ctx.ui.notify("Architect: discovering current tenant apps and data sources...", "info");
  }

  const runCliCommand = createArchitectCliRunner({
    pi,
    ctx,
    environment,
  });
  const discovery = await discoverArchitectTenantState({
    runCliCommand,
  });

  if (!discovery.ok) {
    if (ctx.hasUI) {
      ctx.ui.notify(discovery.error, "error");
    }
    return;
  }

  const outputDir = createArchitectRunDirectoryPath({
    cwd: ctx.cwd,
    brief: brief ?? "app-idea",
    date: new Date(discovery.snapshot.generatedAt),
  });
  const discoveryPath = path.join(outputDir, ARCHITECT_DISCOVERY_FILE_NAME);

  try {
    await fs.mkdir(outputDir, {
      recursive: true,
    });

    await fs.writeFile(
      discoveryPath,
      `${JSON.stringify(discovery.snapshot, null, 2)}\n`,
      "utf8",
    );
  }
  catch (error) {
    if (ctx.hasUI) {
      ctx.ui.notify(
        `Architect failed to write ${discoveryPath}: ${error instanceof Error ? error.message : String(error)}`,
        "error",
      );
    }
    return;
  }

  if (ctx.hasUI) {
    ctx.ui.notify(`Architect: saved discovery snapshot to ${discoveryPath}`, "info");
  }

  await startPlanningWorkflow({
    pi,
    ctx,
    mode: "architect",
    task: brief,
    artifactPath: outputDir,
    initialPrompt: buildArchitectPrompt({
      brief,
      outputDir,
      snapshot: discovery.snapshot,
    }),
  });
}

export default function architectExtension(pi: ExtensionAPI) {
  pi.registerCommand("architect", {
    description: "Analyze tenant data sources and plan Docyrus schema artifacts for an app idea",
    handler: async(args, ctx) => {
      await architectHandler(pi, ctx, args);
    },
  });
}
