import type { ChatMessage, ChatRunStatus, ChatSessionMode } from './chat.js';
import type {
  ProjectContextConnectorRef,
  ProjectContextMcpServerRef,
  ProjectContextPluginRef,
} from './context.js';

export type ProjectKind =
  | 'prototype'
  | 'deck'
  | 'template'
  | 'other'
  | 'image'
  | 'video'
  | 'audio';

export type MediaAspect = '1:1' | '16:9' | '9:16' | '4:3' | '3:4';

export type ProjectPlatform =
  | 'auto'
  | 'responsive'
  | 'web-desktop'
  | 'mobile-ios'
  | 'mobile-android'
  | 'tablet'
  | 'desktop-app';

export type AudioKind = 'music' | 'speech' | 'sfx';

export type ProjectDisplayStatus =
  | 'not_started'
  | 'queued'
  | 'running'
  | 'awaiting_input'
  | 'succeeded'
  | 'failed'
  | 'canceled';

export interface ProjectStatusInfo {
  value: ProjectDisplayStatus;
  updatedAt?: number;
  runId?: string;
}

export interface PromptTemplateMetadataSource {
  repo: string;
  license: string;
  author?: string;
  url?: string;
}

// Subset of a curated PromptTemplate kept on the project so the agent can
// reference it on every turn without re-reading the gallery file. The
// `prompt` field is the (possibly user-edited) body — when the user tunes
// it in the New Project panel before clicking Create, those edits land
// here and become authoritative for the system prompt.
export interface PromptTemplateMetadata {
  id: string;
  surface: 'image' | 'video';
  title: string;
  prompt: string;
  summary?: string;
  category?: string;
  tags?: string[];
  model?: string;
  aspect?: MediaAspect;
  source?: PromptTemplateMetadataSource;
}

export type DesignSystemReviewDecision = 'looks-good' | 'needs-work';

export type DesignSystemReviewTaskStatus = 'queued' | 'sent' | 'failed';

export interface DesignSystemReviewAgentTask {
  status: DesignSystemReviewTaskStatus;
  prompt: string;
  queuedAt: string;
  sentAt?: string;
  error?: string;
}

export interface DesignSystemReviewEntry {
  decision: DesignSystemReviewDecision;
  updatedAt: string;
  feedback?: string;
  files?: string[];
  agentTask?: DesignSystemReviewAgentTask;
}

export interface ProjectMetadata {
  kind: ProjectKind;
  intent?: 'live-artifact';
  fidelity?: 'wireframe' | 'high-fidelity';
  speakerNotes?: boolean;
  slideCount?: string;
  animations?: boolean;
  includeLandingPage?: boolean;
  includeOsWidgets?: boolean;
  templateId?: string;
  templateLabel?: string;
  /** Primary target surface selected at project creation. */
  platform?: ProjectPlatform;
  // Tracks whether the visible name was typed by the user or generated by
  // the create flow. First-prompt auto-naming may only replace generated
  // names so named imports/templates stay stable across conversations.
  nameSource?: 'generated' | 'prompt' | 'user';
  /** Concrete delivery surfaces the artifact must account for. `responsive` is a web breakpoint target, not a native app expansion. */
  platformTargets?: ProjectPlatform[];
  inspirationDesignSystemIds?: string[];
  importedFrom?: 'claude-design' | 'folder' | string;
  entryFile?: string;
  sourceFileName?: string;
  // Folder-import (#597): when set, the project's files live under this
  // absolute path instead of .od/projects/<id>/. OD reads and writes
  // directly inside the user's folder. Stored as the realpath() result so
  // symlinks can't redirect writes after import time.
  baseDir?: string;
  // Project library location that owns baseDir when the project was created
  // under a configured project location root.
  projectLocationId?: string;
  // PR #974: marker stamped by the daemon's HMAC-gated import handler
  // when a folder import passed the desktop-main-process trust gate.
  // Only set on folder-imported projects (`baseDir` set) and only when
  // the import request carried a valid `X-OD-Desktop-Import-Token`
  // signed with the secret the desktop main process registered with the
  // daemon at startup. The desktop `shell.openPath` IPC refuses to
  // forward folder-imported projects whose metadata lacks this marker,
  // so a renderer cannot launder an attacker-chosen baseDir into a
  // file-manager reveal even if a future codepath inadvertently lets
  // it set `baseDir` outside the trusted flow. Privileged: rejected
  // by `POST /api/projects` and `PATCH /api/projects/:id`.
  fromTrustedPicker?: true;
  // Hint stamped by the Home composer working-directory chip. It records
  // where the user wanted the project to live without granting write access
  // to that path; actual filesystem roots still use baseDir/import flows.
  userWorkingDir?: string;
  imageModel?: string;
  imageAspect?: MediaAspect;
  imageStyle?: string;
  videoModel?: string;
  videoLength?: number;
  videoAspect?: MediaAspect;
  audioKind?: AudioKind;
  audioModel?: string;
  audioDuration?: number;
  voice?: string;
  // Curated prompt template the user picked in the image/video tab of the
  // New Project panel. Treated by the system-prompt composer as a stylistic
  // and structural reference for the generation request.
  promptTemplate?: PromptTemplateMetadata;
  // Absolute paths to local code folders the agent can read via --add-dir.
  linkedDirs?: string[];
  // Batch/API-created projects can opt out of the initial discovery form so
  // the first agent turn builds immediately from the submitted brief.
  skipDiscoveryBrief?: boolean;
  // Set when the user submits an unmodified curated example prompt from the
  // gallery. Skips discovery AND requests full-quality direct generation,
  // treating the curated title/brief as the complete creative brief. Honored
  // by both the daemon and contracts (API/BYOK) system-prompt composers so the
  // feature is not mode-dependent.
  examplePrompt?: boolean;
  examplePromptTitle?: string;
  examplePromptBrief?: Record<string, string>;
  // Plugins selected through @ mentions on Home. These are additive
  // context references; the explicit "Use plugin" snapshot, when present,
  // remains the primary executable plugin for the run.
  contextPlugins?: ProjectContextPluginRef[];
  contextMcpServers?: ProjectContextMcpServerRef[];
  contextConnectors?: ProjectContextConnectorRef[];
  // Stored on design-system projects so the review overview can remember
  // which generated sections were accepted or sent back for another pass.
  designSystemReview?: Record<string, DesignSystemReviewEntry>;
}

export interface Project {
  id: string;
  name: string;
  skillId: string | null;
  designSystemId: string | null;
  createdAt: number;
  updatedAt: number;
  status?: ProjectStatusInfo;
  pendingPrompt?: string;
  metadata?: ProjectMetadata;
  // Plan §3.A1 / spec §11.5 — set when the project was created with a
  // plugin pinned (e.g. via PluginLoopHome on the web entry). Lets the
  // UI hide the in-composer plugin rail and render the active plugin
  // as context on user messages instead of re-prompting the user to
  // pick a plugin they already selected.
  appliedPluginSnapshotId?: string;
  customInstructions?: string;
}

export interface ProjectTemplate {
  id: string;
  name: string;
  sourceProjectId?: string;
  files: Array<{ name: string; content: string }>;
  description?: string;
  createdAt: number;
}

export interface ProjectBrowserWorkspaceTab {
  id: string;
  insertAfter?: string | null;
  label: string;
  title?: string;
  url?: string;
  iconUrl?: string;
}

export interface ProjectTabsState {
  tabs: string[];
  active: string | null;
  browserTabs?: ProjectBrowserWorkspaceTab[];
  hasSavedState?: boolean;
  updatedAt?: number;
}

export interface Conversation {
  id: string;
  projectId: string;
  title: string | null;
  sessionMode?: ChatSessionMode;
  messageCount?: number;
  createdAt: number;
  updatedAt: number;
  totalDurationMs?: number;
  latestRun?: {
    status: ChatRunStatus;
    startedAt?: number;
    endedAt?: number;
    durationMs?: number;
  };
}

export interface CreateProjectRequest {
  name: string;
  /** Optional project library location id. Omit or use `default` for .od/projects. */
  projectLocationId?: string;
  skillId?: string | null;
  designSystemId?: string | null;
  pendingPrompt?: string;
  metadata?: ProjectMetadata;
  pluginId?: string;
  appliedPluginSnapshotId?: string;
  pluginInputs?: Record<string, unknown>;
  /** Session mode for the default conversation seeded with the project. */
  conversationMode?: ChatSessionMode;
  customInstructions?: string;
  /** Persisted to metadata.skipDiscoveryBrief for automated project runs. */
  skipDiscoveryBrief?: boolean;
}

export interface UpdateProjectRequest {
  name?: string;
  skillId?: string | null;
  designSystemId?: string | null;
  pendingPrompt?: string | null;
  metadata?: ProjectMetadata | null;
  customInstructions?: string | null;
}

export interface ProjectsResponse {
  projects: Project[];
}

export interface ProjectResponse {
  project: Project;
}

// Response body for `GET /api/projects/:id`. Carries the same `project`
// payload as `ProjectResponse` plus a derived `resolvedDir` so the web
// client can address the on-disk working directory directly (e.g. for
// `shell.openPath` from the desktop bridge). For folder-imported projects
// `resolvedDir === metadata.baseDir`; for native projects it is
// `path.join(<daemon projects root>, project.id)`. Computed server-side via
// `resolveProjectDir(...)` so the web client never reconstructs the path.
export interface ProjectDetailResponse extends ProjectResponse {
  resolvedDir: string;
}

export interface CreateProjectResponse extends ProjectResponse {
  conversationId?: string;
  appliedPluginSnapshotId?: string;
}

export interface ProjectLocation {
  id: string;
  name: string;
  path: string;
  builtIn?: boolean;
}

export interface ProjectLocationsResponse {
  locations: ProjectLocation[];
  removedProjectIds?: string[];
}

export interface UpdateProjectLocationsRequest {
  locations: Array<{ id?: string; name?: string; path: string }>;
}

export interface ProjectManifest {
  schemaVersion: 1;
  id: string;
  name: string;
  createdAt: number;
  updatedAt: number;
  skillId?: string | null;
  designSystemId?: string | null;
}

export interface ScanProjectLocationsResponse {
  scanned: number;
  imported: Project[];
  existing: string[];
  skipped: Array<{ path: string; reason: string }>;
}

// POST /api/import/folder — create a project rooted at an existing local
// folder. The submitted baseDir is stored as the project's metadata.baseDir
// (after realpath canonicalization) and OD reads/writes directly inside it.
// The user owns version control; OD does not snapshot or copy.
export interface ImportFolderRequest {
  baseDir: string;
  name?: string;
  skillId?: string | null;
  designSystemId?: string | null;
}

export interface ImportFolderResponse {
  project: Project;
  conversationId: string;
  entryFile: string | null;
}

export interface ReplaceProjectWorkingDirRequest {
  baseDir: string;
}

export interface ReplaceProjectWorkingDirResponse {
  project: Project;
  baseDir: string;
  entryFile: string | null;
}

export interface ConversationsResponse {
  conversations: Conversation[];
}

export interface ConversationResponse {
  conversation: Conversation;
}

export interface CreateConversationRequest {
  title?: string | null;
  sessionMode?: ChatSessionMode;
  /**
   * Seed the new conversation with another conversation's context by copying
   * its messages. The source must belong to the same project; a missing or
   * foreign id is ignored and an empty conversation is created. Powers the
   * "Side Chat" launcher, which forks the current chat's context into a new
   * conversation.
   */
  seedFromConversationId?: string | null;
  /**
   * When paired with `seedFromConversationId`, copy only source messages up to
   * and including this message. Used by the chat "Fork" action so the new
   * conversation resumes from a specific assistant turn instead of inheriting
   * future follow-ups from the source conversation.
   */
  forkAfterMessageId?: string | null;
  /**
   * Client-supplied snapshot of the messages to seed the fork with, in order,
   * up to and including the fork point. When present, the daemon copies these
   * instead of reading the source conversation from the database by id. This
   * makes "Fork" resilient to a fork point that was never persisted — e.g. an
   * assistant turn whose run errored or had its connection reset before the
   * message reached the database. Without it, such a fork would 404 on
   * `forkAfterMessageId` and silently fail. When absent, the daemon falls back
   * to copying from `seedFromConversationId` (the original Side Chat path).
   */
  seedMessages?: ChatMessage[];
}

export interface UpdateConversationRequest {
  title?: string | null;
  sessionMode?: ChatSessionMode;
}

export interface MessagesResponse {
  messages: ChatMessage[];
}

export type DeployProviderId = 'vercel-self' | 'cloudflare-pages';
export type DeploymentStatus =
  | 'deploying'
  | 'preparing-link'
  | 'ready'
  | 'link-delayed'
  | 'protected'
  | 'failed';

export interface CloudflarePagesConfigHints {
  lastZoneId?: string;
  lastZoneName?: string;
  lastDomainPrefix?: string;
}

export interface CloudflarePagesZoneInfo {
  id: string;
  name: string;
  status?: string;
  type?: string;
}

export interface CloudflarePagesZonesResponse {
  zones: CloudflarePagesZoneInfo[];
  cloudflarePages?: CloudflarePagesConfigHints;
}

export interface CloudflarePagesDeploySelection {
  zoneId: string;
  zoneName: string;
  domainPrefix: string;
}

export type DeploymentLinkStatus =
  | 'ready'
  | 'link-delayed'
  | 'protected'
  | 'failed';

export interface DeploymentLinkInfo {
  url: string;
  status: DeploymentLinkStatus;
  statusMessage?: string;
  reachableAt?: number;
}

export type CloudflarePagesDnsStatus =
  | 'skipped'
  | 'created'
  | 'reused'
  | 'unmarked'
  | 'patched'
  | 'conflict'
  | 'failed';

export type CloudflarePagesDomainStatus =
  | 'skipped'
  | 'pending'
  | 'active'
  | 'conflict'
  | 'failed';

export type CloudflarePagesCustomDomainStatus =
  | 'pending'
  | 'ready'
  | 'conflict'
  | 'failed';

export type CloudflarePagesDnsOwnership = 'marked' | 'unmarked' | 'external';

export interface CloudflarePagesCustomDomainInfo {
  hostname: string;
  url: string;
  zoneId: string;
  zoneName: string;
  domainPrefix: string;
  status: CloudflarePagesCustomDomainStatus;
  statusMessage?: string;
  errorCode?: string;
  errorMessage?: string;
  dnsStatus?: CloudflarePagesDnsStatus;
  dnsRecordId?: string;
  dnsOwnership?: CloudflarePagesDnsOwnership;
  domainStatus?: CloudflarePagesDomainStatus;
  pagesDomainStatus?: string;
  validationData?: unknown;
  verificationData?: unknown;
}

export interface CloudflarePagesDeploymentInfo {
  projectName: string;
  pagesDev: DeploymentLinkInfo;
  customDomain?: CloudflarePagesCustomDomainInfo;
}

export interface DeployConfigResponse {
  providerId: DeployProviderId;
  configured: boolean;
  tokenMask: string;
  teamId: string;
  teamSlug: string;
  accountId?: string;
  projectName?: string;
  cloudflarePages?: CloudflarePagesConfigHints;
  target: 'preview';
}

export interface UpdateDeployConfigRequest {
  providerId?: DeployProviderId;
  token?: string;
  teamId?: string;
  teamSlug?: string;
  accountId?: string;
  projectName?: string;
  cloudflarePages?: CloudflarePagesConfigHints;
}

export interface DeploymentInfo {
  id: string;
  projectId: string;
  fileName: string;
  providerId: DeployProviderId;
  url: string;
  deploymentId?: string;
  deploymentCount: number;
  target: 'preview';
  status: DeploymentStatus;
  statusMessage?: string;
  reachableAt?: number;
  cloudflarePages?: CloudflarePagesDeploymentInfo;
  createdAt: number;
  updatedAt: number;
}

export interface ProjectDeploymentsResponse {
  deployments: DeploymentInfo[];
}

export interface DeployProjectFileRequest {
  fileName: string;
  providerId?: DeployProviderId;
  cloudflarePages?: CloudflarePagesDeploySelection;
}

export interface DeployProjectFileResponse extends DeploymentInfo {}

export interface CheckDeploymentLinkResponse extends DeploymentInfo {}

// Preflight inspects the file set that would be uploaded for a deploy
// without sending anything to the provider. Lets the UI show file count,
// total size, and warnings before the user pays the network round-trip.

export type DeployPreflightWarningCode =
  | 'broken-reference'
  | 'invalid-reference'
  | 'large-asset'
  | 'large-bundle'
  | 'large-html'
  | 'external-script'
  | 'external-stylesheet'
  | 'no-doctype'
  | 'no-viewport';

export interface DeployPreflightWarning {
  code: DeployPreflightWarningCode;
  message: string;
  path?: string;
  url?: string;
  size?: number;
}

export interface DeployPreflightFile {
  path: string;
  size: number;
  mime: string;
  sourcePath: string;
}

export interface DeployPreflightRequest {
  fileName: string;
  providerId?: DeployProviderId;
}

export interface DeployPreflightResponse {
  providerId: DeployProviderId;
  entry: string;
  files: DeployPreflightFile[];
  totalFiles: number;
  totalBytes: number;
  warnings: DeployPreflightWarning[];
}
