summaryrefslogblamecommitdiff
path: root/makima/frontend/src/lib/api.ts
blob: 79685838f2faded82a4dfc4e39f2e1809cc95ade (plain) (tree)
1
2
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883

                                      


































                                                                                 






























































                                                                                     
                                                          
                                                        
                                                                           
                                                                                



                                               










                                  






                                                        

                                                        






                                       

                                                                  
 





                              
                  



                                                                     

                    



                                                








                                

                         
                  





                                                                     









                                    

                                                                                   

                       
                                 
                    

                                                      





                                    

                       




















                                                    

 


                                                                
                 




                              

                              
                   
                          
                               









                               













                                               




                                
                                    



                                                              
                                                          






                                                                
                                                                






                                                                                
                                                           
                   











                                                                
                                                                 
                  

                               





                                                                 






                                                                
                                                                 





                                                                



                                   
                  
                   

                              
                          



                                        


                                      



                                                                      
                   
                               






                                                                   
































                                                                                          
                                                                             






                                                                                             
                                                                                        










                                                                
                                                                                      
                   













                                                                    
















                                                                                                      
































































                                                                                
























                                                                                





                                                     

                                                      






                                 

                                                                  

                                                                    







                       

                                      






































                                                               


                        











                                                

                                                                     





























                                                               

                                                                    







































































































































































                                                                                           









































































































                                                                                       

























































































                                                                                       




































































                                                                                      







                                                                                  










































                                                                                      





































































                                                                                                 






















                                                                                        
                                                                                



































                                                                                





































































                                                                                             













































































































































































































































































































































































                                                                                                     




                                                                                
                                                                   
                                                                  




                                                                                   




                                                                             


                                   




                                                                            
                                               
                                                   
                    

 

















                                                                                

                                                       



                                                 
                                        

 













                                                    
   

                                                                








                                                                               
















                                     

                                                   

                         

                                                      

                                                                                 











                             

                                                   



                                                      



                                                                                   

                                                                                 




























                                                         





                                                                                               

                                                                                 






                                        



                                               






































































































                                                                                

                                                                                          


                                          

                           


                                                                           
                                               






























































































































































































































































                                                                                  





















































                                                                                








































                                                                                








                                                                                

                                                                      



                         

                                                 

                                                                                                                                                 










                                                                                     







































                                                                          









































































                                                                                       

                                                                                












































































































































































































































































































































































































                                                                                                

















































































































                                                                                               











                                                                                






































































































                                                                                                                                                      
 
















                                                                                              


                                  

                                                                

                                                                                     















                                



                                                                                            


















                                                       

                                  

                                                                

                                                                                     



















                                         

                                                                     

                                                                                                     









                                         

                                                               

                                                                                     











































































































































                                                                                                                                         

                                                                                                         

                   
                                                                                 


                    





                                       





                                                                                                   







                                                                                              
                                                                                














































































                                                                                                                          



                                                                                
                                                                                        












                                                                              
                               
                       
















                                     
                      
                        











                                     
                        







                                 
                  
                 





                                                          
                                           
                                        














































                                                                                                  
                                                                                   

                                                                                       




                                                                                     
import { supabase } from "./supabase";

const API_CONFIG = {
  local: {
    http: "http://localhost:8080",
    ws: "ws://localhost:8080",
  },
  production: {
    http: "https://api.makima.jp",
    ws: "wss://api.makima.jp",
  },
} as const;

type Environment = "local" | "production";

function detectEnvironment(): Environment {
  // Check if explicitly set via env var
  const envOverride = import.meta.env.VITE_API_ENV as Environment | undefined;
  if (envOverride && (envOverride === "local" || envOverride === "production")) {
    return envOverride;
  }

  // Auto-detect based on hostname
  if (typeof window !== "undefined") {
    const hostname = window.location.hostname;
    if (hostname === "localhost" || hostname === "127.0.0.1") {
      return "local";
    }
  }

  return "production";
}

const env = detectEnvironment();

export const API_BASE = API_CONFIG[env].http;
export const WS_BASE = API_CONFIG[env].ws;

// =============================================================================
// Authentication helpers
// =============================================================================

/** Storage key for API key */
const API_KEY_STORAGE_KEY = "makima_api_key";

/** Get stored API key from localStorage */
export function getStoredApiKey(): string | null {
  if (typeof window === "undefined") return null;
  return localStorage.getItem(API_KEY_STORAGE_KEY);
}

/** Store API key in localStorage */
export function setStoredApiKey(key: string): void {
  if (typeof window === "undefined") return;
  localStorage.setItem(API_KEY_STORAGE_KEY, key);
}

/** Remove stored API key */
export function clearStoredApiKey(): void {
  if (typeof window === "undefined") return;
  localStorage.removeItem(API_KEY_STORAGE_KEY);
}

/** Get auth headers for API requests */
async function getAuthHeaders(): Promise<HeadersInit> {
  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  // Try Supabase session first
  if (supabase) {
    const { data: { session } } = await supabase.auth.getSession();
    if (session?.access_token) {
      headers["Authorization"] = `Bearer ${session.access_token}`;
      return headers;
    }
  }

  // Fall back to API key if available
  const apiKey = getStoredApiKey();
  if (apiKey) {
    headers["X-Makima-API-Key"] = apiKey;
  }

  return headers;
}

/** Fetch with authentication headers */
async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
  const authHeaders = await getAuthHeaders();
  const mergedHeaders = {
    ...authHeaders,
    ...options.headers,
  };

  return fetch(url, {
    ...options,
    headers: mergedHeaders,
  });
}
export const LISTEN_ENDPOINT = `${WS_BASE}/api/v1/listen`;
export const SPEAK_ENDPOINT = `${WS_BASE}/api/v1/speak`;
export const FILE_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/files/subscribe`;
export const TASK_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/mesh/tasks/subscribe`;

export function getEnvironment(): Environment {
  return env;
}

// File API types
export interface TranscriptEntry {
  id: string;
  speaker: string;
  start: number;
  end: number;
  text: string;
  isFinal: boolean;
}

// Chart types for visualization
export type ChartType = "line" | "bar" | "pie" | "area";

// Body element types for structured content
export type BodyElement =
  | { type: "heading"; level: number; text: string }
  | { type: "paragraph"; text: string }
  | { type: "code"; language?: string; content: string }
  | { type: "list"; ordered: boolean; items: string[] }
  | {
      type: "chart";
      chartType: ChartType;
      title?: string;
      data: Record<string, unknown>[];
      config?: Record<string, unknown>;
    }
  | { type: "image"; src: string; alt?: string; caption?: string }
  | { type: "markdown"; content: string };

export interface FileSummary {
  id: string;
  name: string;
  description: string | null;
  transcriptCount: number;
  duration: number | null;
  version: number;
  /** Path to linked repository file (e.g., "README.md") */
  repoFilePath: string | null;
  /** Sync status: 'none', 'synced', 'modified', 'conflict' */
  repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null;
  createdAt: string;
  updatedAt: string;
  // Contract info (joined from contracts table)
  contractId: string | null;
  contractName: string | null;
  contractPhase: ContractPhase | null;
}

export interface FileDetail {
  id: string;
  ownerId: string;
  name: string;
  description: string | null;
  transcript: TranscriptEntry[];
  location: string | null;
  summary: string | null;
  body: BodyElement[];
  version: number;
  /** Path to linked repository file (e.g., "README.md") */
  repoFilePath: string | null;
  /** When file was last synced from repository */
  repoSyncedAt: string | null;
  /** Sync status: 'none', 'synced', 'modified', 'conflict' */
  repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null;
  createdAt: string;
  updatedAt: string;
}

export interface FileListResponse {
  files: FileSummary[];
  total: number;
}

export interface CreateFileRequest {
  /** Contract this file belongs to (required - files must belong to a contract) */
  contractId: string;
  name?: string;
  description?: string;
  transcript?: TranscriptEntry[];
  location?: string;
  /** Initial body elements (e.g., from a template) */
  body?: BodyElement[];
}

export interface UpdateFileRequest {
  name?: string;
  description?: string;
  transcript?: TranscriptEntry[];
  summary?: string;
  body?: BodyElement[];
  version?: number;
}

// Conflict error types
export interface ConflictErrorResponse {
  code: "VERSION_CONFLICT";
  message: string;
  expectedVersion: number;
  actualVersion: number;
}

export class VersionConflictError extends Error {
  expectedVersion: number;
  actualVersion: number;

  constructor(conflict: ConflictErrorResponse) {
    super(conflict.message);
    this.name = "VersionConflictError";
    this.expectedVersion = conflict.expectedVersion;
    this.actualVersion = conflict.actualVersion;
  }
}

// Available LLM models
export type LlmModel = "claude-sonnet" | "claude-opus" | "groq";

// Chat API types
export interface ChatMessage {
  role: "user" | "assistant";
  content: string;
}

export interface ChatRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
  focusedElementIndex?: number;
}

export interface ToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

// User question types for interactive LLM tool
export interface UserQuestion {
  id: string;
  question: string;
  options: string[];
  allowMultiple: boolean;
  allowCustom: boolean;
}

export interface UserAnswer {
  id: string;
  answers: string[];
}

export interface ChatResponse {
  response: string;
  toolCalls: ToolCallInfo[];
  updatedBody: BodyElement[];
  updatedSummary: string | null;
  pendingQuestions?: UserQuestion[];
}

// File API functions
export async function listFiles(): Promise<FileListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/files`);
  if (!res.ok) {
    throw new Error(`Failed to list files: ${res.statusText}`);
  }
  return res.json();
}

export async function getFile(id: string): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get file: ${res.statusText}`);
  }
  return res.json();
}

export async function createFile(data: CreateFileRequest): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create file: ${res.statusText}`);
  }
  return res.json();
}

export async function updateFile(
  id: string,
  data: UpdateFileRequest
): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update file: ${res.statusText}`);
  }
  return res.json();
}

export async function deleteFile(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete file: ${res.statusText}`);
  }
}

// Chat API function
export async function chatWithFile(
  id: string,
  message: string,
  model?: LlmModel,
  history?: ChatMessage[],
  focusedElementIndex?: number
): Promise<ChatResponse> {
  const body: ChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  if (focusedElementIndex !== undefined) {
    body.focusedElementIndex = focusedElementIndex;
  }
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Version history types
export type VersionSource = "user" | "llm" | "system";

export interface FileVersion {
  version: number;
  name: string;
  description: string | null;
  summary: string | null;
  body: BodyElement[];
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
}

export interface FileVersionSummary {
  version: number;
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
}

export interface FileVersionListResponse {
  versions: FileVersionSummary[];
  total: number;
}

export interface RestoreVersionRequest {
  targetVersion: number;
}

// Version history API functions
export async function listFileVersions(fileId: string): Promise<FileVersionListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions`);
  if (!res.ok) {
    throw new Error(`Failed to list versions: ${res.statusText}`);
  }
  return res.json();
}

export async function getFileVersion(fileId: string, version: number): Promise<FileVersion> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/${version}`);
  if (!res.ok) {
    throw new Error(`Failed to get version: ${res.statusText}`);
  }
  return res.json();
}

export async function restoreFileVersion(
  fileId: string,
  targetVersion: number,
  currentVersion: number
): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/restore`, {
    method: "POST",
    body: JSON.stringify({ targetVersion, currentVersion }),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to restore version: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Sync a file from its linked repository file.
 * Triggers an async operation - the file will be updated when the daemon responds.
 * Returns 202 Accepted if the sync started successfully.
 */
export async function syncFileFromRepo(fileId: string): Promise<{ message: string; fileId: string }> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/sync-from-repo`, {
    method: "POST",
  });

  if (!res.ok) {
    const error = await res.json();
    throw new Error(error.message || `Failed to sync file: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// LLM Tool Definitions for Version History
// =============================================================================
// These types define the tools available to the LLM for version history access.
// The backend should implement handlers for these tools.

/**
 * Tool: read_version
 * Allows the LLM to read the content of a specific historical version.
 * This is read-only - it does not modify the document.
 */
export interface ReadVersionToolInput {
  version: number;
}

export interface ReadVersionToolOutput {
  success: boolean;
  version: number;
  body: BodyElement[];
  summary: string | null;
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
  message: string;
}

/**
 * Tool: list_versions
 * Allows the LLM to list all available versions of the document.
 */
export interface ListVersionsToolOutput {
  success: boolean;
  versions: FileVersionSummary[];
  currentVersion: number;
  message: string;
}

/**
 * Tool: restore_version
 * Allows the LLM to restore the document to a specific historical version.
 * This creates a new version with the content from the target version.
 */
export interface RestoreVersionToolInput {
  targetVersion: number;
  reason?: string;
}

export interface RestoreVersionToolOutput {
  success: boolean;
  previousVersion: number;
  newVersion: number;
  restoredFromVersion: number;
  message: string;
}

// LLM Tool type definitions for the backend
export type LlmVersionTool =
  | { name: "read_version"; input: ReadVersionToolInput }
  | { name: "list_versions"; input: Record<string, never> }
  | { name: "restore_version"; input: RestoreVersionToolInput };

export type LlmVersionToolResult =
  | { name: "read_version"; result: ReadVersionToolOutput }
  | { name: "list_versions"; result: ListVersionsToolOutput }
  | { name: "restore_version"; result: RestoreVersionToolOutput };

// =============================================================================
// Mesh/Task Types for Claude Code Orchestration
// =============================================================================

export type TaskStatus =
  | "pending"
  | "initializing"
  | "starting"
  | "running"
  | "paused"
  | "blocked"
  | "done"
  | "failed"
  | "merged";

export type MergeMode = "pr" | "auto" | "manual";

/** Action to perform when a task completes successfully */
export type CompletionAction = "none" | "branch" | "merge" | "pr";

export type DaemonStatus = "connected" | "disconnected" | "unhealthy";

export interface TaskSummary {
  id: string;
  /** Contract this task belongs to */
  contractId: string | null;
  /** Contract name (joined from contracts table) */
  contractName: string | null;
  /** Contract phase (joined from contracts table) */
  contractPhase: ContractPhase | null;
  /** Contract status (joined from contracts table) */
  contractStatus: ContractStatus | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  status: TaskStatus;
  priority: number;
  progressSummary: string | null;
  subtaskCount: number;
  /** Whether this is a supervisor task (contract orchestrator) */
  isSupervisor: boolean;
  /** Whether this task is hidden from the UI (user dismissed it) */
  hidden: boolean;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface Task {
  id: string;
  ownerId: string;
  /** Contract this task belongs to */
  contractId: string | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  description: string | null;
  status: TaskStatus;
  priority: number;
  plan: string;

  // Daemon/container info
  daemonId: string | null;
  containerId: string | null;
  overlayPath: string | null;

  // Repository info
  repositoryUrl: string | null;
  baseBranch: string | null;
  targetBranch: string | null;

  // Merge settings
  mergeMode: MergeMode | null;
  prUrl: string | null;

  // Completion action settings
  /** Path to user's local repository for completion actions */
  targetRepoPath: string | null;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction: CompletionAction | null;

  // Progress tracking
  progressSummary: string | null;
  lastOutput: string | null;
  errorMessage: string | null;

  // Timestamps
  startedAt: string | null;
  completedAt: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;

  // Supervisor flag
  isSupervisor: boolean;
}

export interface TaskWithSubtasks extends Task {
  subtasks: TaskSummary[];
}

export interface TaskListResponse {
  tasks: TaskSummary[];
  total: number;
}

export interface CreateTaskRequest {
  /** Contract this task belongs to (optional - can be standalone) */
  contractId?: string;
  name: string;
  description?: string;
  plan: string;
  parentTaskId?: string;
  priority?: number;
  repositoryUrl?: string;
  baseBranch?: string;
  targetBranch?: string;
  mergeMode?: MergeMode;
  /** Path to user's local repository for completion actions */
  targetRepoPath?: string;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction?: CompletionAction;
}

export interface UpdateTaskRequest {
  name?: string;
  description?: string;
  plan?: string;
  status?: TaskStatus;
  priority?: number;
  progressSummary?: string;
  lastOutput?: string;
  errorMessage?: string;
  mergeMode?: MergeMode;
  prUrl?: string;
  /** Path to user's local repository for completion actions */
  targetRepoPath?: string;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction?: CompletionAction;
  /** Whether this task is hidden from the UI (user dismissed it) */
  hidden?: boolean;
  version?: number;
}

export interface TaskEvent {
  id: string;
  taskId: string;
  eventType: string;
  previousStatus: string | null;
  newStatus: string | null;
  eventData: Record<string, unknown> | null;
  createdAt: string;
}

export interface TaskEventListResponse {
  events: TaskEvent[];
  total: number;
}

export interface Daemon {
  id: string;
  ownerId: string;
  connectionId: string;
  hostname: string | null;
  machineId: string | null;
  maxConcurrentTasks: number;
  currentTaskCount: number;
  status: DaemonStatus;
  lastHeartbeatAt: string;
  connectedAt: string;
  disconnectedAt: string | null;
}

export interface DaemonListResponse {
  daemons: Daemon[];
  total: number;
}

// Mesh API functions
export async function listTasks(): Promise<TaskListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`);
  if (!res.ok) {
    throw new Error(`Failed to list tasks: ${res.statusText}`);
  }
  return res.json();
}

export async function getTask(id: string): Promise<TaskWithSubtasks> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get task: ${res.statusText}`);
  }
  return res.json();
}

export async function createTask(data: CreateTaskRequest): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create task: ${res.statusText}`);
  }
  return res.json();
}

export async function updateTask(
  id: string,
  data: UpdateTaskRequest
): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update task: ${res.statusText}`);
  }
  return res.json();
}

export async function deleteTask(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete task: ${res.statusText}`);
  }
}

export async function startTask(id: string): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/start`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to start task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function stopTask(id: string): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/stop`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to stop task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export interface SendMessageResponse {
  success: boolean;
  taskId: string;
  messageLength: number;
}

/**
 * Send a message to a running task's stdin.
 * This can be used to provide input to Claude Code when it's waiting for user input,
 * or to inject context/instructions into a running task.
 */
export async function sendTaskMessage(
  taskId: string,
  message: string
): Promise<SendMessageResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/message`, {
    method: "POST",
    body: JSON.stringify({ message }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to send message: ${errorText || res.statusText}`);
  }
  return res.json();
}

export interface RetryCompletionResponse {
  success: boolean;
  taskId: string;
  action: string;
  targetRepoPath: string;
  message: string;
}

/**
 * Retry completion action for a completed task.
 * This allows retrying a completion action (push branch, merge, create PR)
 * after filling in the target_repo_path if it wasn't set when the task completed.
 */
export async function retryCompletionAction(
  taskId: string
): Promise<RetryCompletionResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/retry-completion`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to retry completion action: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Git Actions for Tasks (Manual)
// =============================================================================

/** Response from export patch */
export interface ExportPatchResponse {
  success: boolean;
  taskId: string;
  fileName: string;
  filePath?: string;
  patchSize?: number;
  message: string;
}

/**
 * Export a task's changes as a patch file.
 * The patch will be saved to the contract's patch directory.
 */
export async function exportTaskPatch(
  taskId: string,
  fileName?: string
): Promise<ExportPatchResponse> {
  const body: Record<string, unknown> = {};
  if (fileName) {
    body.fileName = fileName;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/export-patch`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to export patch: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Response from push branch */
export interface PushBranchResponse {
  success: boolean;
  taskId: string;
  branchName: string;
  remote?: string;
  message: string;
}

/**
 * Push a task's changes to a remote branch.
 * Creates a branch if it doesn't exist and pushes the commits.
 */
export async function pushTaskBranch(
  taskId: string,
  branchName?: string
): Promise<PushBranchResponse> {
  const body: Record<string, unknown> = {};
  if (branchName) {
    body.branchName = branchName;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/push-branch`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to push branch: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Response from create PR */
export interface CreatePRResponse {
  success: boolean;
  taskId: string;
  prUrl?: string;
  prNumber?: number;
  branchName?: string;
  message: string;
}

/**
 * Create a pull request for a task's changes.
 * First pushes the branch if needed, then creates the PR.
 */
export async function createTaskPR(
  taskId: string,
  title?: string,
  body?: string
): Promise<CreatePRResponse> {
  const reqBody: Record<string, unknown> = {};
  if (title) {
    reqBody.title = title;
  }
  if (body) {
    reqBody.body = body;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/create-pr`, {
    method: "POST",
    body: JSON.stringify(reqBody),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create PR: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** A suggested directory from a connected daemon */
export interface DaemonDirectory {
  /** Path to the directory */
  path: string;
  /** Display label for the directory */
  label: string;
  /** Type of directory: "working", "makima", "worktrees" */
  directoryType: string;
  /** Daemon hostname this directory is from */
  hostname: string | null;
  /** Whether the directory already exists (for validation) */
  exists?: boolean;
}

export interface DaemonDirectoriesResponse {
  directories: DaemonDirectory[];
}

/**
 * Get suggested directories from connected daemons.
 * These can be used as target_repo_path suggestions for completion actions.
 */
export async function getDaemonDirectories(): Promise<DaemonDirectoriesResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/directories`);
  if (!res.ok) {
    throw new Error(`Failed to get daemon directories: ${res.statusText}`);
  }
  return res.json();
}

/** Request to clone a worktree */
export interface CloneWorktreeRequest {
  targetDir: string;
}

/** Response from clone worktree */
export interface CloneWorktreeResponse {
  status: string;
  taskId: string;
  targetDir: string;
}

/**
 * Clone a task's worktree to a target directory.
 */
export async function cloneWorktree(
  taskId: string,
  targetDir: string
): Promise<CloneWorktreeResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/clone`, {
    method: "POST",
    body: JSON.stringify({ targetDir }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to clone worktree: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Request to check if target exists */
export interface CheckTargetExistsRequest {
  targetDir: string;
}

/** Response from check target exists */
export interface CheckTargetExistsResponse {
  status: string;
  taskId: string;
  targetDir: string;
}

/**
 * Check if a target directory exists.
 */
export async function checkTargetExists(
  taskId: string,
  targetDir: string
): Promise<CheckTargetExistsResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/check-target`, {
    method: "POST",
    body: JSON.stringify({ targetDir }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to check target: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Task Recovery (Daemon Failover)
// =============================================================================

/** Request to reassign a task to a new daemon */
export interface ReassignTaskRequest {
  targetDaemonId?: string;
  includeContext?: boolean;
}

/** Response from reassigning a task */
export interface ReassignTaskResponse {
  task: Task;
  daemonId: string;
  oldTaskId: string;
  contextIncluded: boolean;
  contextEntries: number;
}

/**
 * Reassign a task to a new daemon after daemon disconnect.
 * Creates a new task with conversation context, deletes the old one.
 */
export async function reassignTask(
  taskId: string,
  options?: ReassignTaskRequest
): Promise<ReassignTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/reassign`, {
    method: "POST",
    body: JSON.stringify(options || {}),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to reassign task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Request to continue a task */
export interface ContinueTaskRequest {
  targetDaemonId?: string;
}

/** Response from continuing a task */
export interface ContinueTaskResponse {
  task: Task;
  daemonId: string;
  contextEntries: number;
}

/**
 * Continue a task after daemon disconnect by restarting it with conversation context.
 * Unlike reassign, this keeps the same task ID.
 */
export async function continueTask(
  taskId: string,
  options?: ContinueTaskRequest
): Promise<ContinueTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/continue`, {
    method: "POST",
    body: JSON.stringify(options || {}),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to continue task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function listSubtasks(taskId: string): Promise<TaskListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/subtasks`);
  if (!res.ok) {
    throw new Error(`Failed to list subtasks: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Task Branching
// =============================================================================

/** Request to branch a task */
export interface BranchTaskRequest {
  /** Message to send to the new branched task */
  message: string;
  /** Optional name for the new task (defaults to "Branch of {original task name}") */
  name?: string;
  /** Whether to include conversation history from the source task */
  includeConversation?: boolean;
}

/** Response from branching a task */
export interface BranchTaskResponse {
  /** The newly created task */
  task: Task;
  /** Number of conversation messages copied to the new task */
  messageCount: number;
  /** ID of the daemon assigned to the new task (null if not yet assigned) */
  daemonId: string | null;
}

/**
 * Branch a task to create a new task with the same state.
 * Copies the worktree and optionally the conversation history.
 */
export async function branchTask(
  taskId: string,
  request: BranchTaskRequest
): Promise<BranchTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/branch`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to branch task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function listTaskEvents(
  taskId: string
): Promise<TaskEventListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/events`);
  if (!res.ok) {
    throw new Error(`Failed to list task events: ${res.statusText}`);
  }
  return res.json();
}

/** A single output entry from a Claude Code task */
export interface TaskOutputEntry {
  id: string;
  taskId: string;
  /** Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" */
  messageType: string;
  /** Main text content */
  content: string;
  /** Tool name if tool_use message */
  toolName?: string;
  /** Tool input JSON if tool_use message */
  toolInput?: Record<string, unknown>;
  /** Whether tool result was an error */
  isError?: boolean;
  /** Cost in USD if result message */
  costUsd?: number;
  /** Duration in ms if result message */
  durationMs?: number;
  /** Timestamp when this output was recorded */
  createdAt: string;
}

/** Response from the task output endpoint */
export interface TaskOutputResponse {
  entries: TaskOutputEntry[];
  total: number;
  taskId: string;
}

/**
 * Get task output history.
 * Retrieves all recorded output from a task's Claude Code process.
 * Use this to fetch missed output when subscribing late or reconnecting.
 */
export async function getTaskOutput(
  taskId: string
): Promise<TaskOutputResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/output`);
  if (!res.ok) {
    throw new Error(`Failed to get task output: ${res.statusText}`);
  }
  return res.json();
}

export async function listDaemons(): Promise<DaemonListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons`);
  if (!res.ok) {
    throw new Error(`Failed to list daemons: ${res.statusText}`);
  }
  return res.json();
}

export async function getDaemon(id: string): Promise<Daemon> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get daemon: ${res.statusText}`);
  }
  return res.json();
}

/** Response from the restart daemon endpoint */
export interface RestartDaemonResponse {
  success: boolean;
  daemonId: string;
  message: string;
}

/**
 * Restart a connected daemon.
 * Sends a restart command to the daemon, which will gracefully terminate
 * and restart. Any running tasks will be interrupted.
 */
export async function restartDaemon(id: string): Promise<RestartDaemonResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}/restart`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Failed to restart daemon: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Daemon Platform Download
// =============================================================================

/** A daemon platform with its availability and download URL */
export interface DaemonPlatform {
  platform: string;
  available: boolean;
  downloadUrl: string;
}

/** Response from the list daemon platforms endpoint */
export interface DaemonPlatformsResponse {
  platforms: DaemonPlatform[];
}

/**
 * List available daemon platforms and their download status.
 * This is an unauthenticated endpoint.
 */
export async function listDaemonPlatforms(): Promise<DaemonPlatformsResponse> {
  const res = await fetch(`${API_BASE}/api/v1/daemon/download/platforms`);
  if (!res.ok) {
    throw new Error(`Failed to list daemon platforms: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get the full download URL for a daemon binary.
 * Returns the absolute URL including API_BASE for cross-origin usage.
 */
export function getDaemonDownloadUrl(platform: string): string {
  return `${API_BASE}/api/v1/daemon/download/${platform}`;
}

// =============================================================================
// Daemon Reauthorization
// =============================================================================

/** Response from the trigger daemon reauth endpoint */
export interface TriggerReauthResponse {
  success: boolean;
  daemonId: string;
  requestId: string;
}

/** Response from the reauth status polling endpoint */
export interface ReauthStatusResponse {
  status: string; // "pending" | "url_ready" | "completed" | "failed"
  loginUrl?: string;
  error?: string;
}

/**
 * Trigger OAuth re-authentication on a daemon.
 * Sends a reauth command to the daemon, which will spawn `claude setup-token`
 * and return the OAuth login URL.
 */
export async function triggerDaemonReauth(daemonId: string): Promise<TriggerReauthResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Failed to trigger reauth: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Submit an OAuth auth code to a daemon's pending reauth flow.
 */
export async function submitDaemonAuthCode(
  daemonId: string,
  code: string,
  requestId: string,
): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/code`, {
    method: "POST",
    body: JSON.stringify({ code, requestId }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Failed to submit auth code: ${res.statusText}`);
  }
}

/**
 * Get the status of a daemon reauth request.
 * Used for polling to track reauth flow progress.
 */
export async function getDaemonReauthStatus(
  daemonId: string,
  requestId: string,
): Promise<ReauthStatusResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/${requestId}/status`,
  );
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Failed to get reauth status: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Mesh Chat Types for Task Orchestration
// =============================================================================

export interface MeshChatMessage {
  role: "user" | "assistant";
  content: string;
}

export interface MeshChatRequest {
  message: string;
  model?: LlmModel;
  history?: MeshChatMessage[];
}

export interface MeshToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

export interface MeshChatResponse {
  response: string;
  toolCalls: MeshToolCallInfo[];
  pendingQuestions?: UserQuestion[];
}

// Mesh Chat API functions

// Top-level mesh chat (no specific task context)
export async function chatWithMesh(
  message: string,
  model?: LlmModel,
  history?: MeshChatMessage[]
): Promise<MeshChatResponse> {
  const body: MeshChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Task-scoped mesh chat
export async function chatWithTask(
  taskId: string,
  message: string,
  model?: LlmModel,
  history?: MeshChatMessage[]
): Promise<MeshChatResponse> {
  const body: MeshChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Mesh Chat History Types
// =============================================================================

export type MeshChatContextType = "mesh" | "task" | "subtask";

export interface MeshChatContext {
  type: MeshChatContextType;
  taskId?: string;
  parentTaskId?: string;
}

export interface MeshChatMessageRecord {
  id: string;
  conversationId: string;
  role: "user" | "assistant" | "error";
  content: string;
  contextType: MeshChatContextType;
  contextTaskId: string | null;
  toolCalls: MeshToolCallInfo[] | null;
  pendingQuestions: UserQuestion[] | null;
  createdAt: string;
}

export interface MeshChatHistoryResponse {
  conversationId: string;
  messages: MeshChatMessageRecord[];
}

export interface MeshChatWithContextRequest {
  message: string;
  model?: LlmModel;
  contextType?: MeshChatContextType;
  contextTaskId?: string;
}

// =============================================================================
// Mesh Chat History API Functions
// =============================================================================

/**
 * Get the current chat history from the database
 */
export async function getMeshChatHistory(): Promise<MeshChatHistoryResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`);
  if (!res.ok) {
    throw new Error(`Failed to get chat history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Clear chat history (archives current conversation, starts new one)
 */
export async function clearMeshChatHistory(): Promise<{ success: boolean; conversationId: string }> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to clear chat history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Chat with mesh using context (new approach with DB history)
 */
export async function chatWithMeshContext(
  message: string,
  context: MeshChatContext,
  model?: LlmModel
): Promise<MeshChatResponse> {
  const body: MeshChatWithContextRequest = {
    message,
    contextType: context.type,
  };

  if (model) {
    body.model = model;
  }

  // Set contextTaskId based on context type
  if (context.type === "task" && context.taskId) {
    body.contextTaskId = context.taskId;
  } else if (context.type === "subtask" && context.taskId) {
    body.contextTaskId = context.taskId;
  }

  // Use top-level endpoint (it now loads history from DB)
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// API Key Management
// =============================================================================

export interface ApiKeyInfo {
  id: string;
  prefix: string;
  name: string | null;
  lastUsedAt: string | null;
  createdAt: string;
}

export interface CreateApiKeyResponse {
  id: string;
  key: string;
  prefix: string;
  name: string | null;
  createdAt: string;
}

export interface RefreshApiKeyResponse {
  id: string;
  key: string;
  prefix: string;
  name: string | null;
  createdAt: string;
  previousKeyRevoked: boolean;
}

export interface RevokeApiKeyResponse {
  message: string;
  revokedKeyPrefix: string;
}

/**
 * Get information about the current active API key.
 */
export async function getApiKey(): Promise<ApiKeyInfo | null> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`);
  if (res.status === 404) {
    return null;
  }
  if (!res.ok) {
    throw new Error(`Failed to get API key: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a new API key.
 */
export async function createApiKey(name?: string): Promise<CreateApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
    method: "POST",
    body: JSON.stringify({ name }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Refresh (rotate) the current API key.
 */
export async function refreshApiKey(name?: string): Promise<RefreshApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys/refresh`, {
    method: "POST",
    body: JSON.stringify({ name }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to refresh API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Revoke the current API key.
 */
export async function revokeApiKey(): Promise<RevokeApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
    method: "DELETE",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to revoke API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// User Account Management
// =============================================================================

export interface ChangePasswordRequest {
  currentPassword: string;
  newPassword: string;
}

export interface ChangePasswordResponse {
  success: boolean;
  message: string;
}

export interface ChangeEmailRequest {
  password: string;
  newEmail: string;
}

export interface ChangeEmailResponse {
  success: boolean;
  message: string;
  verificationSent: boolean;
}

export interface DeleteAccountRequest {
  password: string;
  confirmation: string;
}

export interface DeleteAccountResponse {
  success: boolean;
  message: string;
}

/**
 * Change the current user's password.
 * Requires current password verification.
 */
export async function changePassword(
  currentPassword: string,
  newPassword: string
): Promise<ChangePasswordResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me/password`, {
    method: "PUT",
    body: JSON.stringify({ currentPassword, newPassword }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

/**
 * Change the current user's email address.
 * Requires password verification.
 */
export async function changeEmail(
  password: string,
  newEmail: string
): Promise<ChangeEmailResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me/email`, {
    method: "PUT",
    body: JSON.stringify({ password, newEmail }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

/**
 * Delete the current user's account.
 * Requires password verification and email confirmation.
 */
export async function deleteAccount(
  password: string,
  confirmation: string
): Promise<DeleteAccountResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me`, {
    method: "DELETE",
    body: JSON.stringify({ password, confirmation }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

// =============================================================================
// Contract Types for Workflow Management
// =============================================================================

/** Contract type determines the workflow and required documents */
export type ContractType = "simple" | "specification" | "execute";
export type ContractPhase = "research" | "specify" | "plan" | "execute" | "review";
export type ContractStatus = "active" | "completed" | "archived";
export type RepositorySourceType = "remote" | "local" | "managed";
export type RepositoryStatus = "ready" | "pending" | "creating" | "failed";

/** Get valid phases for a contract type */
export function getValidPhases(contractType: ContractType): ContractPhase[] {
  if (contractType === "simple") {
    return ["plan", "execute"];
  }
  if (contractType === "execute") {
    return ["execute"];
  }
  return ["research", "specify", "plan", "execute", "review"];
}

/** Get default initial phase for a contract type */
export function getDefaultPhase(contractType: ContractType): ContractPhase {
  if (contractType === "simple") return "plan";
  if (contractType === "execute") return "execute";
  return "research";
}

// =============================================================================
// Contract Type Templates
// =============================================================================

/** Contract type template returned by the API */
export interface ContractTypeTemplate {
  /** Template identifier (e.g., "simple", "specification") */
  id: string;
  /** Display name */
  name: string;
  /** Description of the contract type workflow */
  description: string;
  /** Ordered list of phases for this contract type */
  phases: ContractPhase[];
  /** Default starting phase */
  defaultPhase: ContractPhase;
  /** Whether this is a built-in type (always available) */
  isBuiltin: boolean;
  /** Optional mapping from phase ID to display name */
  phaseNames?: Record<string, string>;
}

/** Response from list contract types endpoint */
export interface ListContractTypesResponse {
  contractTypes: ContractTypeTemplate[];
}

/** Phase definition for custom templates */
export interface PhaseDefinition {
  id: string;
  name: string;
  order: number;
}

/** Deliverable definition for custom templates */
export interface DeliverableDefinition {
  id: string;
  name: string;
  priority: "required" | "recommended" | "optional";
}

/**
 * List available contract types.
 * Returns built-in types only (simple, specification, execute).
 */
export async function listContractTypes(): Promise<ListContractTypesResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/contract-types`);
  if (!res.ok) {
    throw new Error(`Failed to list contract types: ${res.statusText}`);
  }
  return res.json();
}

export interface ContractRepository {
  id: string;
  contractId: string;
  name: string;
  repositoryUrl: string | null;
  localPath: string | null;
  sourceType: RepositorySourceType;
  status: RepositoryStatus;
  isPrimary: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface ContractSummary {
  id: string;
  name: string;
  description: string | null;
  /** Contract type: "simple" or "specification" */
  contractType: ContractType;
  phase: ContractPhase;
  status: ContractStatus;
  /** Supervisor task ID for contract orchestration */
  supervisorTaskId: string | null;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly: boolean;
  fileCount: number;
  taskCount: number;
  repositoryCount: number;
  version: number;
  createdAt: string;
}

export interface Contract {
  id: string;
  ownerId: string;
  name: string;
  description: string | null;
  /** Contract type: "simple" or "specification" */
  contractType: ContractType;
  phase: ContractPhase;
  status: ContractStatus;
  /** Supervisor task ID for contract orchestration */
  supervisorTaskId: string | null;
  /** Whether tasks for this contract should run in autonomous loop mode */
  autonomousLoop: boolean;
  /** Whether to wait for user confirmation before progressing to the next phase */
  phaseGuard: boolean;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly: boolean;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface ContractWithRelations extends Contract {
  repositories: ContractRepository[];
  files: FileSummary[];
  tasks: TaskSummary[];
}

export interface ContractEvent {
  id: string;
  contractId: string;
  eventType: string;
  previousPhase: string | null;
  newPhase: string | null;
  eventData: Record<string, unknown> | null;
  createdAt: string;
}

export interface ContractListResponse {
  contracts: ContractSummary[];
  total: number;
}

export interface CreateContractRequest {
  name: string;
  description?: string;
  /** Contract type: "simple" (default), "specification", "execute", or custom template name */
  contractType?: ContractType | string;
  /** UUID of a custom template to use. If provided, takes precedence over contractType. */
  templateId?: string;
  /** Initial phase to start in (defaults based on contract type or template) */
  initialPhase?: ContractPhase | string;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly?: boolean;
}

export interface UpdateContractRequest {
  name?: string;
  description?: string;
  phase?: ContractPhase;
  status?: ContractStatus;
  /** Enable or disable autonomous loop mode */
  autonomousLoop?: boolean;
  /** Enable or disable phase guard mode */
  phaseGuard?: boolean;
  version?: number;
}

export interface AddRemoteRepositoryRequest {
  name: string;
  repositoryUrl: string;
  isPrimary?: boolean;
}

export interface AddLocalRepositoryRequest {
  name: string;
  localPath: string;
  isPrimary?: boolean;
}

export interface CreateManagedRepositoryRequest {
  name: string;
  isPrimary?: boolean;
}

export interface ChangePhaseRequest {
  phase: ContractPhase;
}

// =============================================================================
// Contract API Functions
// =============================================================================

/**
 * List all contracts.
 */
export async function listContracts(): Promise<ContractListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts`);
  if (!res.ok) {
    throw new Error(`Failed to list contracts: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get a contract with all its relations.
 */
export async function getContract(id: string): Promise<ContractWithRelations> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a new contract.
 */
export async function createContract(
  data: CreateContractRequest
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Update a contract.
 */
export async function updateContract(
  id: string,
  data: UpdateContractRequest
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a contract.
 */
export async function deleteContract(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete contract: ${res.statusText}`);
  }
}

/**
 * Change contract phase.
 * @param confirmed - When true, confirms the phase change even if phase_guard is enabled.
 *                    Defaults to true for explicit user actions like drag-and-drop.
 */
export async function changeContractPhase(
  id: string,
  phase: ContractPhase,
  confirmed: boolean = true
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/phase`, {
    method: "POST",
    body: JSON.stringify({ phase, confirmed }),
  });
  if (!res.ok) {
    throw new Error(`Failed to change phase: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get contract event history.
 */
export async function getContractEvents(
  id: string
): Promise<ContractEvent[]> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/events`);
  if (!res.ok) {
    throw new Error(`Failed to get events: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Contract Repository Management
// =============================================================================

/**
 * Add a remote repository to a contract.
 */
export async function addRemoteRepository(
  contractId: string,
  data: AddRemoteRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/remote`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add remote repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Add a local repository to a contract.
 */
export async function addLocalRepository(
  contractId: string,
  data: AddLocalRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/local`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add local repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a managed repository (daemon will create it).
 */
export async function createManagedRepository(
  contractId: string,
  data: CreateManagedRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/managed`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to create managed repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a repository from a contract.
 */
export async function deleteContractRepository(
  contractId: string,
  repoId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete repository: ${res.statusText}`);
  }
}

/**
 * Set a repository as primary.
 */
export async function setRepositoryPrimary(
  contractId: string,
  repoId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}/primary`,
    {
      method: "PUT",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to set repository as primary: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Task Association
// =============================================================================

/**
 * Add a task to a contract.
 */
export async function addTaskToContract(
  contractId: string,
  taskId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`,
    {
      method: "POST",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add task to contract: ${res.statusText}`);
  }
}

/**
 * Remove a task from a contract.
 */
export async function removeTaskFromContract(
  contractId: string,
  taskId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to remove task from contract: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Chat Types and API
// =============================================================================

export interface ContractChatRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
}

export interface ContractToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

export interface ContractChatResponse {
  response: string;
  toolCalls: ContractToolCallInfo[];
  pendingQuestions?: UserQuestion[];
}

/**
 * Chat with a contract using LLM-powered management tools.
 */
export async function chatWithContract(
  contractId: string,
  message: string,
  model?: LlmModel,
  history?: ChatMessage[]
): Promise<ContractChatResponse> {
  const body: ContractChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Contract chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Contract chat history types
export interface ContractChatMessage {
  id: string;
  conversationId: string;
  role: "user" | "assistant" | "error";
  content: string;
  toolCalls?: unknown;
  pendingQuestions?: unknown;
  createdAt: string;
}

export interface ContractChatHistoryResponse {
  contractId: string;
  conversationId: string;
  messages: ContractChatMessage[];
}

/** Get contract chat history */
export async function getContractChatHistory(
  contractId: string
): Promise<ContractChatHistoryResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/chat/history`
  );
  if (!res.ok) {
    throw new Error(`Failed to fetch contract chat history: ${res.statusText}`);
  }
  return res.json();
}

/** Clear contract chat history (starts a new conversation) */
export async function clearContractChatHistory(
  contractId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/chat/history`,
    { method: "DELETE" }
  );
  if (!res.ok) {
    throw new Error(`Failed to clear contract chat history: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Discussion Types and API
// =============================================================================

export interface DiscussContractRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
  transcriptContext?: string;
}

export interface CreatedContractInfo {
  id: string;
  name: string;
  description: string | null;
  contractType: string;
  initialPhase: string;
}

export interface DiscussContractResponse {
  response: string;
  toolCalls: ContractToolCallInfo[];
  createdContract?: CreatedContractInfo;
  pendingQuestions?: UserQuestion[];
}

/**
 * Discuss a potential contract with Makima.
 * This is an ephemeral conversation that can result in contract creation.
 */
export async function discussContract(
  message: string,
  model?: LlmModel,
  history?: ChatMessage[],
  transcriptContext?: string
): Promise<DiscussContractResponse> {
  const body: DiscussContractRequest = { message };
  if (model) body.model = model;
  if (history && history.length > 0) body.history = history;
  if (transcriptContext) body.transcriptContext = transcriptContext;

  const res = await authFetch(`${API_BASE}/api/v1/contracts/discuss`, {
    method: "POST",
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Discussion failed: ${errorText || res.statusText}`);
  }

  return res.json();
}

// =============================================================================
// Template Types and API
// =============================================================================

export interface TemplateSummary {
  id: string;
  name: string;
  phase: ContractPhase;
  description: string;
  elementCount: number;
}

export interface FileTemplate {
  id: string;
  name: string;
  phase: ContractPhase;
  description: string;
  suggestedBody: BodyElement[];
}

export interface ListTemplatesResponse {
  templates: TemplateSummary[];
}

export async function listTemplates(
  phase?: ContractPhase
): Promise<ListTemplatesResponse> {
  const params = phase ? `?phase=${phase}` : "";
  const res = await authFetch(`${API_BASE}/api/v1/templates${params}`);
  if (!res.ok) {
    throw new Error(`Failed to list templates: ${res.statusText}`);
  }
  return res.json();
}

export async function getTemplate(id: string): Promise<FileTemplate> {
  const res = await authFetch(`${API_BASE}/api/v1/templates/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get template: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Supervisor Question Types and Functions
// =============================================================================

export interface PendingQuestion {
  questionId: string;
  taskId: string;
  contractId: string;
  /** Directive this question relates to (if from a directive task) */
  directiveId?: string | null;
  question: string;
  choices: string[];
  context: string | null;
  createdAt: string;
  /** Whether multiple choices can be selected */
  multiSelect?: boolean;
  /** Question type - "general" for regular questions, "phase_confirmation" for phase transitions, "contract_complete" for contract completion */
  questionType?: "general" | "phase_confirmation" | "contract_complete";
  /** Phase confirmation specific data (when questionType is "phase_confirmation") */
  phaseConfirmation?: {
    currentPhase: ContractPhase;
    nextPhase: ContractPhase;
    contractName?: string;
    summary?: string;
    deliverables?: Array<{
      name: string;
      completed: boolean;
    }>;
  };
}

export interface AnswerQuestionRequest {
  response: string;
}

export interface AnswerQuestionResponse {
  success: boolean;
}

/**
 * Get all pending supervisor questions for the current user.
 */
export async function listPendingQuestions(): Promise<PendingQuestion[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/questions`);
  if (!res.ok) {
    throw new Error(`Failed to list questions: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Answer a pending supervisor question.
 */
export async function answerQuestion(
  questionId: string,
  response: string
): Promise<AnswerQuestionResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/mesh/questions/${questionId}/answer`,
    {
      method: "POST",
      body: JSON.stringify({ response }),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to answer question: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Repository History Types and API
// =============================================================================

export interface RepositoryHistoryEntry {
  id: string;
  ownerId: string;
  name: string;
  repositoryUrl: string | null;
  localPath: string | null;
  sourceType: "remote" | "local";
  useCount: number;
  lastUsedAt: string;
  createdAt: string;
}

export interface RepositoryHistoryListResponse {
  entries: RepositoryHistoryEntry[];
  total: number;
}

/**
 * List all repository history entries.
 * Returns entries ordered by use_count DESC, last_used_at DESC.
 */
export async function listRepositoryHistory(): Promise<RepositoryHistoryListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/settings/repository-history`);
  if (!res.ok) {
    throw new Error(`Failed to list repository history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get repository suggestions based on history.
 * Optionally filter by source type and search query.
 */
export async function getRepositorySuggestions(
  sourceType?: "remote" | "local",
  query?: string,
  limit?: number
): Promise<RepositoryHistoryListResponse> {
  const params = new URLSearchParams();
  if (sourceType) params.append("source_type", sourceType);
  if (query) params.append("query", query);
  if (limit) params.append("limit", limit.toString());

  const queryString = params.toString();
  const url = `${API_BASE}/api/v1/settings/repository-history/suggestions${
    queryString ? `?${queryString}` : ""
  }`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get repository suggestions: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a repository history entry.
 */
export async function deleteRepositoryHistory(id: string): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/settings/repository-history/${id}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete repository history: ${res.statusText}`);
  }
}

// =============================================================================
// History Types
// =============================================================================

/** History event from the timeline */
export interface HistoryEvent {
  id: string;
  ownerId: string;
  contractId: string | null;
  taskId: string | null;
  eventType: string;
  eventSubtype: string | null;
  phase: string | null;
  eventData: Record<string, unknown>;
  createdAt: string;
}

/** Response for contract history endpoint */
export interface ContractHistoryResponse {
  contractId: string;
  entries: HistoryEvent[];
  totalCount: number;
  cursor: string | null;
}

/** Tool call info in conversation messages */
export interface ToolCallInfo {
  id: string;
  name: string;
  input: Record<string, unknown>;
}

/** Conversation message with optional tool calls */
export interface ConversationMessage {
  id: string;
  role: string;
  content: string;
  timestamp: string;
  toolCalls?: ToolCallInfo[];
  toolName?: string;
  toolInput?: Record<string, unknown>;
  toolResult?: string;
  isError?: boolean;
  tokenCount?: number;
  costUsd?: number;
}

/** Reference to a spawned task in supervisor conversation */
export interface TaskReference {
  taskId: string;
  taskName: string;
  status: string;
  createdAt: string;
  completedAt: string | null;
}

/** Response for supervisor conversation endpoint */
export interface SupervisorConversationResponse {
  contractId: string;
  supervisorTaskId: string;
  phase: string;
  lastActivity: string;
  pendingTaskIds: string[];
  messages: ConversationMessage[];
  spawnedTasks: TaskReference[];
}

/** Response for task conversation endpoint */
export interface TaskConversationResponse {
  taskId: string;
  taskName: string;
  status: string;
  messages: ConversationMessage[];
  totalTokens: number | null;
  totalCost: number | null;
}

/** Query filters for timeline endpoint */
export interface TimelineQueryFilters {
  contractId?: string;
  taskId?: string;
  includeSubtasks?: boolean;
  from?: string;
  to?: string;
  limit?: number;
}

/** Query filters for contract history endpoint */
export interface HistoryQueryFilters {
  phase?: string;
  eventTypes?: string;
  from?: string;
  to?: string;
  limit?: number;
}

/** Task checkpoint */
export interface TaskCheckpoint {
  id: string;
  taskId: string;
  checkpointNumber: number;
  commitSha: string;
  branchName: string;
  message: string;
  filesChanged: Array<{ path: string; action: string }>;
  linesAdded: number;
  linesRemoved: number;
  createdAt: string;
}

// =============================================================================
// History API Functions
// =============================================================================

/**
 * Get contract history timeline.
 */
export async function getContractHistory(
  contractId: string,
  filters?: HistoryQueryFilters
): Promise<ContractHistoryResponse> {
  const params = new URLSearchParams();
  if (filters?.phase) params.append("phase", filters.phase);
  if (filters?.eventTypes) params.append("event_types", filters.eventTypes);
  if (filters?.from) params.append("from", filters.from);
  if (filters?.to) params.append("to", filters.to);
  if (filters?.limit) params.append("limit", filters.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/contracts/${contractId}/history${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get contract history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get supervisor conversation history.
 */
export async function getSupervisorConversation(
  contractId: string
): Promise<SupervisorConversationResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation`
  );
  if (!res.ok) {
    throw new Error(`Failed to get supervisor conversation: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get task conversation history.
 */
export async function getTaskConversation(
  taskId: string,
  options?: { includeToolCalls?: boolean; includeToolResults?: boolean; limit?: number }
): Promise<TaskConversationResponse> {
  const params = new URLSearchParams();
  if (options?.includeToolCalls !== undefined)
    params.append("include_tool_calls", options.includeToolCalls.toString());
  if (options?.includeToolResults !== undefined)
    params.append("include_tool_results", options.includeToolResults.toString());
  if (options?.limit) params.append("limit", options.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/mesh/tasks/${taskId}/conversation${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get task conversation: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get unified timeline for current user.
 */
export async function getTimeline(
  filters?: TimelineQueryFilters
): Promise<ContractHistoryResponse> {
  const params = new URLSearchParams();
  if (filters?.contractId) params.append("contract_id", filters.contractId);
  if (filters?.taskId) params.append("task_id", filters.taskId);
  if (filters?.includeSubtasks !== undefined)
    params.append("include_subtasks", filters.includeSubtasks.toString());
  if (filters?.from) params.append("from", filters.from);
  if (filters?.to) params.append("to", filters.to);
  if (filters?.limit) params.append("limit", filters.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/timeline${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get timeline: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get task checkpoints.
 */
export async function getTaskCheckpoints(taskId: string): Promise<TaskCheckpoint[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints`);
  if (!res.ok) {
    throw new Error(`Failed to get task checkpoints: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Resume/Rewind/Fork Types
// =============================================================================

/** Request to rewind a task to a checkpoint */
export interface RewindTaskRequest {
  checkpointId?: string;
  checkpointSha?: string;
  preserveMode: "discard" | "create_branch" | "stash";
  branchName?: string;
}

/** Response from task rewind */
export interface RewindTaskResponse {
  taskId: string;
  rewindedTo: {
    checkpointNumber: number;
    sha: string;
    message: string;
  };
  preservedAs?: {
    stateType: string;
    reference: string;
  };
}

/** Request to fork a task from a checkpoint */
export interface ForkTaskRequest {
  forkFromType: "checkpoint" | "timestamp" | "message_id";
  forkFromValue: string;
  newTaskName: string;
  newTaskPlan: string;
  includeConversation?: boolean;
  createBranch?: boolean;
  branchName?: string;
}

/** Response from task fork */
export interface ForkTaskResponse {
  newTaskId: string;
  sourceTaskId: string;
  forkPoint: {
    forkType: string;
    checkpoint?: TaskCheckpoint;
    timestamp: string;
  };
  branchName?: string;
  conversationIncluded: boolean;
  messageCount?: number;
}

/** Request to resume supervisor */
export interface ResumeSupervisorRequest {
  resumeMode: "continue" | "restart_phase" | "from_checkpoint";
  checkpointId?: string;
  additionalContext?: string;
}

/** Response from supervisor resume */
export interface ResumeSupervisorResponse {
  supervisorTaskId: string;
  daemonId: string | null;
  contractId: string;
  phase: string;
  status: string;
  conversationMessageCount: number;
}

/** Request to rewind supervisor conversation */
export interface RewindConversationRequest {
  toMessageId?: string;
  byMessageCount?: number;
  rewindCode?: boolean;
}

/** Response from conversation rewind */
export interface RewindConversationResponse {
  contractId: string;
  messagesRemoved: number;
  newMessageCount: number;
}

// =============================================================================
// Resume/Rewind/Fork API Functions
// =============================================================================

/**
 * Rewind a task to a checkpoint.
 */
export async function rewindTask(
  taskId: string,
  request: RewindTaskRequest
): Promise<RewindTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/rewind`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to rewind task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Fork a task from a checkpoint.
 */
export async function forkTask(
  taskId: string,
  request: ForkTaskRequest
): Promise<ForkTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/fork`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to fork task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Resume a supervisor.
 */
export async function resumeSupervisor(
  contractId: string,
  request: ResumeSupervisorRequest
): Promise<ResumeSupervisorResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/resume`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to resume supervisor: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Rewind supervisor conversation.
 */
export async function rewindSupervisorConversation(
  contractId: string,
  request: RewindConversationRequest
): Promise<RewindConversationResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation/rewind`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to rewind conversation: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Resume task from a checkpoint.
 */
export async function resumeFromCheckpoint(
  taskId: string,
  checkpointId: string,
  request: { taskName?: string; plan: string; includeConversation?: boolean }
): Promise<{ taskId: string }> {
  const res = await authFetch(
    `${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints/${checkpointId}/resume`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to resume from checkpoint: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Supervisor/Autopilot Control Functions
// =============================================================================

/**
 * Start a contract's supervisor task (enable autopilot mode).
 * This is a convenience wrapper around startTask.
 */
export async function startSupervisor(supervisorTaskId: string): Promise<Task> {
  return startTask(supervisorTaskId);
}

/**
 * Stop a contract's supervisor task (pause autopilot mode).
 * This is a convenience wrapper around stopTask.
 */
export async function stopSupervisor(supervisorTaskId: string): Promise<Task> {
  return stopTask(supervisorTaskId);
}

/** Status of the supervisor/autopilot for a contract */
export interface SupervisorStatus {
  supervisorTaskId: string | null;
  status: "not_configured" | "pending" | "starting" | "running" | "paused" | "done" | "failed";
  daemonId: string | null;
  canStart: boolean;
  canStop: boolean;
  canResume: boolean;
}

/**
 * Get the supervisor status for a contract.
 */
export function getSupervisorStatus(
  contract: ContractWithRelations
): SupervisorStatus {
  const supervisorTaskId = contract.supervisorTaskId;

  if (!supervisorTaskId) {
    return {
      supervisorTaskId: null,
      status: "not_configured",
      daemonId: null,
      canStart: false,
      canStop: false,
      canResume: false,
    };
  }

  // Find the supervisor task in the contract's tasks
  const supervisorTask = contract.tasks.find(
    (t) => t.id === supervisorTaskId && t.isSupervisor
  );

  if (!supervisorTask) {
    return {
      supervisorTaskId,
      status: "pending",
      daemonId: null,
      canStart: true,
      canStop: false,
      canResume: false,
    };
  }

  // Map task status to supervisor status
  let status: SupervisorStatus["status"];
  let canStart = false;
  let canStop = false;
  let canResume = false;

  switch (supervisorTask.status) {
    case "pending":
      status = "pending";
      canStart = true;
      break;
    case "initializing":
    case "starting":
      status = "starting";
      canStop = true;
      break;
    case "running":
      status = "running";
      canStop = true;
      break;
    case "paused":
    case "blocked":
      status = "paused";
      canResume = true;
      canStop = true;
      break;
    case "done":
    case "merged":
      status = "done";
      break;
    case "failed":
      status = "failed";
      canResume = true;
      break;
    default:
      status = "pending";
      canStart = true;
  }

  return {
    supervisorTaskId,
    status,
    daemonId: null, // Task summary doesn't have daemon_id, would need full task
    canStart,
    canStop,
    canResume,
  };
}

// =============================================================================
// Task Dismiss (Hide) Functions
// =============================================================================

/**
 * Dismiss (hide) a completed standalone task from the UI.
 * This marks the task as hidden so it won't appear in the task list.
 */
export async function dismissTask(taskId: string): Promise<Task> {
  return updateTask(taskId, { hidden: true });
}

// =============================================================================
// Worktree Info Types and Functions
// =============================================================================

/** File status in the worktree (git status) */
export type FileStatus = "M" | "A" | "D" | "R" | "C" | "U" | "?" | "modified" | "added" | "deleted" | "renamed" | "copied" | "unmerged" | "untracked";

/** A single changed file in the worktree */
export interface WorktreeFile {
  /** File path relative to worktree root */
  path: string;
  /** Git status code (M=modified, A=added, D=deleted, R=renamed, C=copied, U=unmerged, ?=untracked) */
  status: FileStatus;
  /** Lines added (0 if deleted or unavailable) */
  linesAdded: number;
  /** Lines removed (0 if added or unavailable) */
  linesRemoved: number;
}

/** Statistics about worktree changes */
export interface WorktreeStats {
  /** Number of files changed */
  filesChanged: number;
  /** Total lines inserted */
  insertions: number;
  /** Total lines deleted */
  deletions: number;
}

/** Worktree information for a task */
export interface WorktreeInfo {
  /** Task ID */
  taskId: string;
  /** Path to the worktree directory */
  worktreePath: string | null;
  /** Whether the worktree exists on the daemon */
  exists: boolean;
  /** Aggregate statistics */
  stats: WorktreeStats;
  /** Changed files list */
  files: WorktreeFile[];
  /** Current branch name */
  branch: string | null;
  /** Current HEAD commit SHA */
  headSha: string | null;
}

/**
 * Get worktree information for a task.
 * Returns changed files, stats, and metadata about the worktree.
 */
export async function getWorktreeInfo(taskId: string): Promise<WorktreeInfo> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/worktree-info`);
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to get worktree info: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Patch Types and Functions
// =============================================================================

/** Summary of a patch file (contract file of type "patch") */
export interface PatchSummary {
  /** Patch/file ID */
  id: string;
  /** Patch name */
  name: string;
  /** Optional description */
  description: string | null;
  /** Task ID this patch was created from */
  taskId: string | null;
  /** Contract ID */
  contractId: string;
  /** Number of files in the patch */
  filesCount: number;
  /** Total lines added */
  linesAdded: number;
  /** Total lines removed */
  linesRemoved: number;
  /** List of file paths in the patch (if available) */
  files: string[] | null;
  /** When the patch was created */
  createdAt: string;
  /** When the patch was last updated */
  updatedAt: string;
}

/**
 * List patches for a task.
 * Returns contract files of type "patch" associated with the task.
 */
export async function listTaskPatches(taskId: string, contractId: string): Promise<PatchSummary[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/patches?contractId=${contractId}`);
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to list patches: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Directive Types & API
// =============================================================================

export type DirectiveStatus = "draft" | "active" | "idle" | "paused" | "archived";
export type StepStatus = "pending" | "ready" | "running" | "completed" | "failed" | "skipped";

export interface Directive {
  id: string;
  ownerId: string;
  title: string;
  goal: string;
  status: DirectiveStatus;
  repositoryUrl: string | null;
  localPath: string | null;
  baseBranch: string | null;
  orchestratorTaskId: string | null;
  prUrl: string | null;
  prBranch: string | null;
  completionTaskId: string | null;
  /** Whether the memory system is enabled for this directive */
  memoryEnabled: boolean;
  /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */
  reconcileMode: string;
  goalUpdatedAt: string;
  startedAt: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface DirectiveStep {
  id: string;
  directiveId: string;
  name: string;
  description: string | null;
  taskPlan: string | null;
  dependsOn: string[];
  status: StepStatus;
  taskId: string | null;
  /** Contract ID for contract-backed steps */
  contractId?: string | null;
  /** Contract type (e.g. "simple", "specification", "execute") for contract-backed steps */
  contractType?: string | null;
  orderIndex: number;
  generation: number;
  startedAt: string | null;
  completedAt: string | null;
  createdAt: string;
}

export interface DirectiveWithSteps extends Directive {
  steps: DirectiveStep[];
}

export interface DirectiveSummary {
  id: string;
  ownerId: string;
  title: string;
  goal: string;
  status: DirectiveStatus;
  repositoryUrl: string | null;
  orchestratorTaskId: string | null;
  prUrl: string | null;
  completionTaskId: string | null;
  /** Whether the memory system is enabled for this directive */
  memoryEnabled: boolean;
  /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */
  reconcileMode: string;
  version: number;
  createdAt: string;
  updatedAt: string;
  totalSteps: number;
  completedSteps: number;
  runningSteps: number;
  failedSteps: number;
}

export interface DirectiveListResponse {
  directives: DirectiveSummary[];
  total: number;
}

export interface CreateDirectiveRequest {
  title: string;
  goal: string;
  repositoryUrl?: string;
  localPath?: string;
  baseBranch?: string;
  /** Enable the memory system for this directive (default: false) */
  memoryEnabled?: boolean;
  /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) (default: auto) */
  reconcileMode?: string;
}

export interface UpdateDirectiveRequest {
  title?: string;
  goal?: string;
  status?: string;
  repositoryUrl?: string;
  localPath?: string;
  baseBranch?: string;
  orchestratorTaskId?: string;
  /** Enable or disable the memory system for this directive */
  memoryEnabled?: boolean;
  /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */
  reconcileMode?: string;
  version?: number;
}

export interface CreateDirectiveStepRequest {
  name: string;
  description?: string;
  taskPlan?: string;
  dependsOn?: string[];
  orderIndex?: number;
  generation?: number;
}

export interface UpdateDirectiveStepRequest {
  name?: string;
  description?: string;
  taskPlan?: string;
  dependsOn?: string[];
  status?: string;
  taskId?: string;
  orderIndex?: number;
}

export async function listDirectives(): Promise<DirectiveListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives`);
  if (!res.ok) throw new Error(`Failed to list directives: ${res.statusText}`);
  return res.json();
}

export async function createDirective(req: CreateDirectiveRequest): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to create directive: ${res.statusText}`);
  return res.json();
}

export async function getDirective(id: string): Promise<DirectiveWithSteps> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`);
  if (!res.ok) throw new Error(`Failed to get directive: ${res.statusText}`);
  return res.json();
}

export async function updateDirective(id: string, req: UpdateDirectiveRequest): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to update directive: ${res.statusText}`);
  return res.json();
}

export async function deleteDirective(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { method: "DELETE" });
  if (!res.ok) throw new Error(`Failed to delete directive: ${res.statusText}`);
}

export async function createDirectiveStep(directiveId: string, req: CreateDirectiveStepRequest): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to create step: ${res.statusText}`);
  return res.json();
}

export async function batchCreateDirectiveSteps(directiveId: string, steps: CreateDirectiveStepRequest[]): Promise<DirectiveStep[]> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/batch`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(steps),
  });
  if (!res.ok) throw new Error(`Failed to batch create steps: ${res.statusText}`);
  return res.json();
}

export async function updateDirectiveStep(directiveId: string, stepId: string, req: UpdateDirectiveStepRequest): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to update step: ${res.statusText}`);
  return res.json();
}

export async function deleteDirectiveStep(directiveId: string, stepId: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, { method: "DELETE" });
  if (!res.ok) throw new Error(`Failed to delete step: ${res.statusText}`);
}

export async function startDirective(id: string): Promise<DirectiveWithSteps> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/start`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to start directive: ${res.statusText}`);
  return res.json();
}

export async function pauseDirective(id: string): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/pause`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to pause directive: ${res.statusText}`);
  return res.json();
}

export async function advanceDirective(id: string): Promise<DirectiveWithSteps> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/advance`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to advance directive: ${res.statusText}`);
  return res.json();
}

export async function completeDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/complete`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to complete step: ${res.statusText}`);
  return res.json();
}

export async function failDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/fail`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to fail step: ${res.statusText}`);
  return res.json();
}

export async function skipDirectiveStep(directiveId: string, stepId: string): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to skip step: ${res.statusText}`);
  return res.json();
}

export async function updateDirectiveGoal(id: string, goal: string): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/goal`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ goal }),
  });
  if (!res.ok) throw new Error(`Failed to update goal: ${res.statusText}`);
  return res.json();
}

export async function cleanupDirective(id: string): Promise<{ message: string; taskId: string | null }> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/cleanup`, {
    method: "POST",
  });
  if (!res.ok) throw new Error(`Failed to cleanup directive: ${res.statusText}`);
  return res.json();
}

export interface PickUpOrdersResponse {
  message: string;
  orderCount: number;
  taskId: string | null;
}

export async function createDirectivePR(id: string): Promise<DirectiveWithSteps> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/create-pr`, { method: "POST" });
  if (!res.ok) throw new Error(`Failed to create PR: ${res.statusText}`);
  return res.json();
}

export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/pick-up-orders`, {
    method: "POST",
  });
  if (!res.ok) throw new Error(`Failed to pick up orders: ${res.statusText}`);
  return res.json();
}

// =============================================================================
// Directive Order Groups (DOGs) API
// =============================================================================

export type DOGStatus = "open" | "in_progress" | "done" | "archived";

export interface DirectiveOrderGroup {
  id: string;
  directiveId: string;
  ownerId: string;
  name: string;
  description: string | null;
  status: DOGStatus;
  createdAt: string;
  updatedAt: string;
}

export interface DOGListResponse {
  dogs: DirectiveOrderGroup[];
}

export interface CreateDOGRequest {
  name: string;
  description?: string | null;
}

export interface UpdateDOGRequest {
  name?: string;
  description?: string | null;
  status?: DOGStatus;
}

export async function listDogs(directiveId: string): Promise<DOGListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`);
  if (!res.ok) throw new Error(`Failed to list DOGs: ${res.statusText}`);
  return res.json();
}

export async function createDog(directiveId: string, req: CreateDOGRequest): Promise<DirectiveOrderGroup> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to create DOG: ${res.statusText}`);
  return res.json();
}

export async function getDog(directiveId: string, dogId: string): Promise<DirectiveOrderGroup> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`);
  if (!res.ok) throw new Error(`Failed to get DOG: ${res.statusText}`);
  return res.json();
}

export async function updateDog(directiveId: string, dogId: string, req: UpdateDOGRequest): Promise<DirectiveOrderGroup> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to update DOG: ${res.statusText}`);
  return res.json();
}

export async function deleteDog(directiveId: string, dogId: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, {
    method: "DELETE",
  });
  if (!res.ok) throw new Error(`Failed to delete DOG: ${res.statusText}`);
}

export async function pickUpDogOrders(directiveId: string, dogId: string): Promise<PickUpOrdersResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}/pick-up-orders`, {
    method: "POST",
  });
  if (!res.ok) throw new Error(`Failed to pick up DOG orders: ${res.statusText}`);
  return res.json();
}

// =============================================================================
// Orders API
// =============================================================================

export type OrderPriority = "critical" | "high" | "medium" | "low" | "none";
export type OrderStatus = "open" | "in_progress" | "under_review" | "done" | "archived";
export type OrderType = "feature" | "bug" | "spike" | "chore" | "improvement";

export interface Order {
  id: string;
  ownerId: string;
  title: string;
  description: string | null;
  priority: OrderPriority;
  status: OrderStatus;
  orderType: OrderType;
  labels: string[];
  directiveId: string | null;
  directiveStepId: string | null;
  directiveName: string | null;
  dogId: string | null;
  repositoryUrl: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface OrderListResponse {
  orders: Order[];
  total: number;
}

export interface CreateOrderRequest {
  title: string;
  description?: string | null;
  priority?: OrderPriority;
  status?: OrderStatus;
  orderType?: OrderType;
  labels?: string[];
  directiveId: string;
  dogId?: string | null;
  repositoryUrl?: string | null;
}

export interface UpdateOrderRequest {
  title?: string;
  description?: string | null;
  priority?: OrderPriority;
  status?: OrderStatus;
  orderType?: OrderType;
  labels?: string[];
  directiveId?: string | null;
  directiveStepId?: string | null;
  dogId?: string | null;
  repositoryUrl?: string | null;
}

export async function listOrders(
  status?: OrderStatus,
  type?: OrderType,
  priority?: OrderPriority,
  directiveId?: string,
  search?: string,
  dogId?: string,
): Promise<OrderListResponse> {
  const params = new URLSearchParams();
  if (status) params.set("status", status);
  if (type) params.set("type", type);
  if (priority) params.set("priority", priority);
  if (directiveId) params.set("directiveId", directiveId);
  if (search) params.set("search", search);
  if (dogId) params.set("dogId", dogId);
  const qs = params.toString();
  const res = await authFetch(`${API_BASE}/api/v1/orders${qs ? `?${qs}` : ""}`);
  if (!res.ok) throw new Error(`Failed to list orders: ${res.statusText}`);
  return res.json();
}

export async function createOrder(req: CreateOrderRequest): Promise<Order> {
  const res = await authFetch(`${API_BASE}/api/v1/orders`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to create order: ${res.statusText}`);
  return res.json();
}

export async function getOrder(id: string): Promise<Order> {
  const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`);
  if (!res.ok) throw new Error(`Failed to get order: ${res.statusText}`);
  return res.json();
}

export async function updateOrder(id: string, req: UpdateOrderRequest): Promise<Order> {
  const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`Failed to update order: ${res.statusText}`);
  return res.json();
}

export async function deleteOrder(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`, { method: "DELETE" });
  if (!res.ok) throw new Error(`Failed to delete order: ${res.statusText}`);
}

export async function linkOrderToDirective(orderId: string, directiveId: string): Promise<Order> {
  const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/link-directive`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ directiveId }),
  });
  if (!res.ok) throw new Error(`Failed to link order to directive: ${res.statusText}`);
  return res.json();
}

export async function convertOrderToStep(orderId: string): Promise<DirectiveStep> {
  const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/convert-to-step`, {
    method: "POST",
  });
  if (!res.ok) throw new Error(`Failed to convert order to step: ${res.statusText}`);
  return res.json();
}