import { z } from 'zod';
import { type APIClient, APIResponseSchema } from '../api.ts';
import { ProjectResponseError } from './util.ts';

export const Resources = z.object({
	memory: z.string().default('500Mi').describe('The memory requirements'),
	cpu: z.string().default('500m').describe('The CPU requirements'),
	disk: z.string().default('500Mi').describe('The disk requirements'),
});

export const Mode = z.object({
	type: z
		.enum(['on-demand', 'provisioned'])
		.default('on-demand')
		.describe('on-demand or provisioned'),
	idle: z.string().optional().describe('Idle timeout duration when on-demand (e.g., "10m", "1h")'),
});

export const ProjectBuildConfig = z.object({
	timeout: z.string().optional().describe('Build execution timeout (e.g. "30m")'),
	resources: z
		.object({
			memory: z.string().optional().describe('Build sandbox memory (e.g. "4Gi")'),
			cpu: z.string().optional().describe('Build sandbox CPU (e.g. "2")'),
			disk: z.string().optional().describe('Build sandbox disk (e.g. "4Gi")'),
		})
		.optional()
		.describe('Build sandbox resource limits'),
});

export const DeploymentConfig = z.object({
	resources: Resources.optional().describe('the resource requirements for your deployed project'),
	mode: Mode.optional().describe('the provisioning mode for the project'),
	dependencies: z
		.array(z.string().describe('APT dependencies to install prior to launching your project'))
		.optional(),
	domains: z.array(z.string().describe('the custom domain')).optional(),
});

const BaseFileFields = {
	filename: z.string().describe('the relative path for the file'),
	version: z.string().describe('the SHA256 content of the file'),
};

export const BuildEvalSchema = z.object({
	...BaseFileFields,
	id: z.string().describe('the unique calculated id for the eval'),
	identifier: z.string().describe('the unique id for eval for the project across deployments'),
	name: z.string().describe('the name of the eval'),
	description: z.string().optional().describe('the eval description'),
	agentIdentifier: z.string().describe('the identifier of the agent'),
	projectId: z.string().describe('the project id'),
});

const BaseAgentFields = {
	...BaseFileFields,
	id: z.string().describe('the unique calculated id for the agent'),
	agentId: z.string().describe('the unique id for agent for the project across deployments'),
	projectId: z.string().describe('the project id'),
	name: z.string().describe('the name of the agent'),
	description: z.string().optional().describe('the agent description'),
	evals: z.array(BuildEvalSchema).optional().describe('the evals for the agent'),
	schema: z
		.object({
			input: z.string().optional().describe('JSON schema for input (stringified JSON)'),
			output: z.string().optional().describe('JSON schema for output (stringified JSON)'),
		})
		.optional()
		.describe('input and output JSON schemas for the agent'),
};

export const BuildAgentSchema = z.object({
	...BaseAgentFields,
});

export const BuildMetadataSchema = z.object({
	routes: z.array(
		z.object({
			id: z.string().describe('the unique calculated id for the route'),
			filename: z.string().describe('the relative path for the file'),
			path: z.string().describe('the route path'),
			method: z.enum(['get', 'post', 'put', 'delete', 'patch']).describe('the HTTP method'),
			version: z.string().describe('the SHA256 content of the file'),
			type: z.enum(['api', 'sms', 'email', 'cron', 'websocket', 'sse', 'stream']),
			agentIds: z
				.array(z.string())
				.optional()
				.describe('the agent ids associated with this route'),
			config: z
				.record(z.string(), z.unknown())
				.optional()
				.describe('type specific configuration'),
			schema: z
				.object({
					input: z.string().optional().describe('JSON schema for input (stringified JSON)'),
					output: z.string().optional().describe('JSON schema for output (stringified JSON)'),
				})
				.optional()
				.describe('input and output JSON schemas for the route'),
		})
	),
	agents: z.array(BuildAgentSchema),
	assets: z.array(
		z.object({
			filename: z.string().describe('the relative path for the file'),
			kind: z.string().describe('the type of asset'),
			contentType: z.string().describe('the content-type for the file'),
			contentEncoding: z.string().optional().describe('the content-encoding for the file'),
			size: z.number().describe('the size in bytes for the file'),
		})
	),
	project: z.object({
		id: z.string().describe('the project id'),
		name: z.string().describe('the name of the project (from package.json)'),
		version: z.string().optional().describe('the version of the project (from package.json)'),
		description: z
			.string()
			.optional()
			.describe('the description of the project (from package.json)'),
		keywords: z.array(z.string()).optional().describe('the keywords from package.json'),
		orgId: z.string().describe('the organization id for the project'),
	}),
	deployment: z.intersection(
		DeploymentConfig,
		z.object({
			id: z.string().describe('the deployment id'),
			date: z.string().describe('the date the deployment was created in UTC format'),
			git: z
				.object({
					repo: z.string().optional().describe('the repository name'),
					commit: z.string().optional().describe('the git commit sha'),
					message: z.string().optional().describe('the git commit message'),
					branch: z.string().optional().describe('the git branch'),
					tags: z.array(z.string()).optional().describe('the tags for the current branch'),
					pr: z.string().optional().describe('the pull request number'),
					provider: z.string().optional().describe('the CI provider'),
					trigger: z
						.string()
						.default('cli')
						.optional()
						.describe('the trigger that caused the build'),
					url: z
						.string()
						.url()
						.optional()
						.describe('the url to the commit for the CI provider'),
					buildUrl: z
						.string()
						.url()
						.optional()
						.describe('the url to the build for the CI provider'),
					event: z
						.enum(['pull_request', 'push', 'manual', 'workflow'])
						.default('manual')
						.optional()
						.describe(
							'The type of Git-related event that triggered the deployment: pull_request (A pull request or merge request was opened, updated, or merged), push (A commit was pushed directly to a branch), manual (A deployment was triggered manually via CLI or a button), workflow (A deployment was triggered by an automated workflow, such as a CI pipeline)'
						),
					pull_request: z
						.object({
							number: z.number(),
							url: z.string().optional(),
						})
						.optional()
						.describe(
							'This is only present when the deployment was triggered via a pull request.'
						),
				})
				.optional()
				.describe('git commit information'),
			build: z.object({
				bun: z.string().describe('the version of bun that was used to build the deployment'),
				agentuity: z.string().describe('the version of the agentuity runtime'),
				arch: z.string().describe('the machine architecture'),
				platform: z.string().describe('the machine os platform'),
			}),
		})
	),
});

export type BuildMetadata = z.infer<typeof BuildMetadataSchema>;

export const CreateProjectDeploymentSchema = z.object({
	id: z.string().describe('the unique id for the deployment'),
	orgId: z.string().describe('the organization id'),
	publicKey: z.string().describe('the public key to use for encrypting the deployment'),
	buildLogsStreamURL: z
		.string()
		.optional()
		.describe('the URL for streaming build logs (PUT to write, GET to read)'),
});

export const CreateProjectDeploymentResponseSchema = APIResponseSchema(
	CreateProjectDeploymentSchema
);

type CreateProjectDeploymentPayload = z.infer<typeof CreateProjectDeploymentResponseSchema>;

export type Deployment = z.infer<typeof CreateProjectDeploymentSchema>;

/**
 * Create a new project deployment
 *
 * @param client
 * @param projectId
 * @returns
 */
export async function projectDeploymentCreate(
	client: APIClient,
	projectId: string,
	deploymentConfig?: z.infer<typeof DeploymentConfig>
): Promise<Deployment> {
	const resp = await client.request<CreateProjectDeploymentPayload>(
		'POST',
		`/cli/deploy/2/start/${projectId}`,
		CreateProjectDeploymentResponseSchema,
		deploymentConfig ?? {}
	);
	if (resp.success) {
		return resp.data;
	}
	throw new ProjectResponseError({ message: resp.message });
}

export const DeploymentInstructionsSchema = z.object({
	deployment: z.string().describe('the url for uploading the encrypted deployment archive'),
	assets: z
		.record(
			z.string().describe('the asset id'),
			z.string().describe('the url for the asset upload')
		)
		.describe('the upload metadata for public assets'),
});

export const DeploymentInstructionsResponseSchema = APIResponseSchema(DeploymentInstructionsSchema);

type DeploymentInstructionsResponse = z.infer<typeof DeploymentInstructionsResponseSchema>;
export type DeploymentInstructions = z.infer<typeof DeploymentInstructionsSchema>;

/**
 * Update the deployment with the build metadata
 *
 * @param client
 * @param deploymentId
 * @returns
 */
export async function projectDeploymentUpdate(
	client: APIClient,
	deploymentId: string,
	deployment: BuildMetadata,
	signal?: AbortSignal
): Promise<DeploymentInstructions> {
	const resp = await client.request<DeploymentInstructionsResponse, BuildMetadata>(
		'PUT',
		`/cli/deploy/2/start/${deploymentId}`,
		DeploymentInstructionsResponseSchema,
		deployment,
		BuildMetadataSchema,
		signal
	);
	if (resp.success) {
		return resp.data;
	}
	throw new ProjectResponseError({ message: resp.message });
}

export const DeploymentCompleteSchema = z.object({
	streamId: z.string().optional().describe('the stream id for warmup logs'),
	publicUrls: z
		.object({
			latest: z.string().url().describe('the public url for the latest deployment'),
			deployment: z.string().url().describe('the public url for this deployment'),
			custom: z.array(z.string().describe('the custom domain')),
			vanityDeployment: z
				.string()
				.url()
				.nullable()
				.optional()
				.describe('the vanity url for this deployment'),
			vanityProject: z
				.string()
				.url()
				.nullable()
				.optional()
				.describe('the vanity url for the latest deployment'),
		})
		.describe('the map of public urls'),
});

export const DeploymentCompleteResponseSchema = APIResponseSchema(DeploymentCompleteSchema);

type DeploymentCompleteResponse = z.infer<typeof DeploymentCompleteResponseSchema>;
export type DeploymentComplete = z.infer<typeof DeploymentCompleteSchema>;

export const DeploymentStateValue = z.enum([
	'pending',
	'building',
	'deploying',
	'failed',
	'completed',
]);

export type DeploymentState = z.infer<typeof DeploymentStateValue>;

export const DeploymentStatusSchema = z.object({
	state: DeploymentStateValue.describe('the current deployment state'),
});

export const DeploymentStatusResponseSchema = APIResponseSchema(DeploymentStatusSchema);

type DeploymentStatusResponse = z.infer<typeof DeploymentStatusResponseSchema>;
export type DeploymentStatusResult = z.infer<typeof DeploymentStatusSchema>;

/**
 * Complete the deployment once build is uploaded
 *
 * @param client
 * @param deploymentId
 * @returns
 */
export async function projectDeploymentComplete(
	client: APIClient,
	deploymentId: string,
	signal?: AbortSignal
): Promise<DeploymentComplete> {
	const resp = await client.request<DeploymentCompleteResponse>(
		'POST',
		`/cli/deploy/2/complete/${deploymentId}`,
		DeploymentCompleteResponseSchema,
		undefined,
		undefined,
		signal
	);
	if (resp.success) {
		return resp.data;
	}
	throw new ProjectResponseError({
		message: resp.message || 'Deployment completion failed with unknown error',
	});
}

/**
 * Get the current provisioning status of a deployment
 *
 * @param client
 * @param deploymentId
 * @returns
 */
export async function projectDeploymentStatus(
	client: APIClient,
	deploymentId: string,
	signal?: AbortSignal
): Promise<DeploymentStatusResult> {
	const resp = await client.request<DeploymentStatusResponse>(
		'GET',
		`/cli/deploy/2/status/${deploymentId}`,
		DeploymentStatusResponseSchema,
		undefined,
		undefined,
		signal
	);
	if (resp.success) {
		return resp.data;
	}
	throw new ProjectResponseError({ message: resp.message });
}

export const ClientDiagnosticsErrorSchema = z.object({
	type: z.enum(['file', 'general']),
	scope: z.enum(['typescript', 'ast', 'build', 'bundler', 'validation', 'deploy']),
	path: z.string().optional(),
	line: z.number().optional(),
	column: z.number().optional(),
	message: z.string(),
	code: z.string().optional(),
});

export const ClientDiagnosticsTimingSchema = z.object({
	name: z.string(),
	startedAt: z.string(),
	completedAt: z.string(),
	durationMs: z.number(),
});

export const ClientDiagnosticsSchema = z.object({
	success: z.boolean(),
	errors: z.array(ClientDiagnosticsErrorSchema),
	warnings: z.array(ClientDiagnosticsErrorSchema),
	diagnostics: z.array(ClientDiagnosticsTimingSchema),
	error: z.string().optional(),
});

export const DeploymentFailPayloadSchema = z.object({
	error: z.string().optional(),
	diagnostics: ClientDiagnosticsSchema.optional(),
});

export type ClientDiagnosticsError = z.infer<typeof ClientDiagnosticsErrorSchema>;
export type ClientDiagnosticsTiming = z.infer<typeof ClientDiagnosticsTimingSchema>;
export type ClientDiagnostics = z.infer<typeof ClientDiagnosticsSchema>;
export type DeploymentFailPayload = z.infer<typeof DeploymentFailPayloadSchema>;

export const DeploymentFailResponseSchema = z.object({
	state: z.literal('failed'),
});

export const DeploymentFailAPIResponseSchema = APIResponseSchema(DeploymentFailResponseSchema);
type DeploymentFailResponse = z.infer<typeof DeploymentFailAPIResponseSchema>;

/**
 * Report a deployment failure from the client
 *
 * @param client
 * @param deploymentId
 * @param payload - Error message and/or structured diagnostics
 * @returns
 */
export async function projectDeploymentFail(
	client: APIClient,
	deploymentId: string,
	payload: DeploymentFailPayload
): Promise<void> {
	const resp = await client.request<DeploymentFailResponse, DeploymentFailPayload>(
		'POST',
		`/cli/deploy/2/fail/${deploymentId}`,
		DeploymentFailAPIResponseSchema,
		payload,
		DeploymentFailPayloadSchema
	);
	if (!resp.success) {
		throw new ProjectResponseError({ message: resp.message });
	}
}
