openapi: "3.0.3"
info:
  title: Meadowlark Platform API
  description: |
    Agentic-first construction management platform API.
    
    Designed to be consumed by both human frontends and autonomous agents.
    All agent actions are logged via `/api/v1/agent-events` for full auditability.
    
    ## Authentication
    All protected endpoints require a Bearer JWT obtained via `POST /api/v1/auth/login`.
    Agents should use a dedicated service-account user with role `agent`.
    
    ## Rate Limits
    - Standard: 1000 req/min per token
    - Agent writes (POST /comms, POST /agent-events): 500 req/min
    
    ## Versioning
    This is v1. Breaking changes will increment the version prefix.
  version: "0.1.0"
  contact:
    name: Meadowlark Platform
    email: dev@meadowlarkconstruction.ca

servers:
  - url: https://app.usetenon.com/api/v1
    description: Production
  - url: http://localhost:8080/api/v1
    description: Local development

tags:
  - name: meta
    description: Health and documentation
  - name: auth
    description: Authentication and identity
  - name: projects
    description: Construction project management
  - name: contacts
    description: Clients, subs, suppliers, employees
  - name: tasks
    description: Project tasks and work items
  - name: comms
    description: Communication log (inbound/outbound)
  - name: agent-events
    description: Agent action audit trail

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          example: "not found"

    Project:
      type: object
      properties:
        id:          { type: integer, format: int64 }
        name:        { type: string }
        status:      { $ref: '#/components/schemas/ProjectStatus' }
        address:     { type: string }
        client_id:   { type: integer, format: int64, nullable: true }
        created_at:  { type: string, format: date-time }
        updated_at:  { type: string, format: date-time }

    ProjectStatus:
      type: string
      enum: [active, completed, on_hold, cancelled]

    ProjectWithSummary:
      allOf:
        - $ref: '#/components/schemas/Project'
        - type: object
          properties:
            task_summary:
              type: object
              properties:
                total:       { type: integer }
                open:        { type: integer }
                in_progress: { type: integer }
                done:        { type: integer }

    CreateProjectRequest:
      type: object
      required: [name]
      properties:
        name:      { type: string }
        status:    { $ref: '#/components/schemas/ProjectStatus' }
        address:   { type: string }
        client_id: { type: integer, format: int64 }

    UpdateProjectRequest:
      type: object
      properties:
        name:      { type: string }
        status:    { $ref: '#/components/schemas/ProjectStatus' }
        address:   { type: string }
        client_id: { type: integer, format: int64 }

    Contact:
      type: object
      properties:
        id:         { type: integer, format: int64 }
        name:       { type: string }
        email:      { type: string, format: email }
        phone:      { type: string }
        type:       { $ref: '#/components/schemas/ContactType' }
        company:    { type: string }
        created_at: { type: string, format: date-time }

    ContactType:
      type: string
      enum: [client, sub, supplier, employee]

    CreateContactRequest:
      type: object
      required: [name]
      properties:
        name:    { type: string }
        email:   { type: string, format: email }
        phone:   { type: string }
        type:    { $ref: '#/components/schemas/ContactType' }
        company: { type: string }

    UpdateContactRequest:
      type: object
      properties:
        name:    { type: string }
        email:   { type: string }
        phone:   { type: string }
        type:    { $ref: '#/components/schemas/ContactType' }
        company: { type: string }

    Task:
      type: object
      properties:
        id:          { type: integer, format: int64 }
        project_id:  { type: integer, format: int64 }
        title:       { type: string }
        description: { type: string }
        status:      { $ref: '#/components/schemas/TaskStatus' }
        priority:    { $ref: '#/components/schemas/TaskPriority' }
        type:        { type: string }
        assignee_id: { type: integer, format: int64, nullable: true }
        due_date:    { type: string, format: date-time, nullable: true }
        created_at:  { type: string, format: date-time }
        updated_at:  { type: string, format: date-time }

    TaskStatus:
      type: string
      enum: [open, in_progress, done, cancelled]

    TaskPriority:
      type: string
      enum: [low, medium, high, urgent]

    CreateTaskRequest:
      type: object
      required: [project_id, title]
      properties:
        project_id:  { type: integer, format: int64 }
        title:       { type: string }
        description: { type: string }
        status:      { $ref: '#/components/schemas/TaskStatus' }
        priority:    { $ref: '#/components/schemas/TaskPriority' }
        type:        { type: string }
        assignee_id: { type: integer, format: int64 }
        due_date:    { type: string, format: date-time }

    UpdateTaskRequest:
      type: object
      properties:
        title:       { type: string }
        description: { type: string }
        status:      { $ref: '#/components/schemas/TaskStatus' }
        priority:    { $ref: '#/components/schemas/TaskPriority' }
        type:        { type: string }
        assignee_id: { type: integer, format: int64 }
        due_date:    { type: string, format: date-time }

    Comm:
      type: object
      properties:
        id:         { type: integer, format: int64 }
        project_id: { type: integer, format: int64 }
        contact_id: { type: integer, format: int64, nullable: true }
        channel:    { $ref: '#/components/schemas/CommChannel' }
        direction:  { $ref: '#/components/schemas/CommDirection' }
        subject:    { type: string }
        body:       { type: string }
        metadata:   { type: object, nullable: true }
        created_at: { type: string, format: date-time }

    CommChannel:
      type: string
      enum: [email, slack, buildern, sms]

    CommDirection:
      type: string
      enum: [in, out]

    CreateCommRequest:
      type: object
      required: [project_id, channel, direction]
      properties:
        project_id: { type: integer, format: int64 }
        contact_id: { type: integer, format: int64 }
        channel:    { $ref: '#/components/schemas/CommChannel' }
        direction:  { $ref: '#/components/schemas/CommDirection' }
        subject:    { type: string }
        body:       { type: string }
        metadata:   { type: object }

    AgentEvent:
      type: object
      properties:
        id:          { type: integer, format: int64 }
        agent_name:  { type: string }
        action:      { type: string }
        entity_type: { type: string }
        entity_id:   { type: integer, format: int64, nullable: true }
        payload:     { type: object, nullable: true }
        status:      { $ref: '#/components/schemas/AgentEventStatus' }
        created_at:  { type: string, format: date-time }

    AgentEventStatus:
      type: string
      enum: [pending, done, failed]

    CreateAgentEventRequest:
      type: object
      required: [agent_name, action]
      properties:
        agent_name:  { type: string, example: "comms-agent" }
        action:      { type: string, example: "email.sent" }
        entity_type: { type: string, example: "project" }
        entity_id:   { type: integer, format: int64 }
        payload:     { type: object }
        status:      { $ref: '#/components/schemas/AgentEventStatus' }

    UpdateAgentEventRequest:
      type: object
      properties:
        status:  { $ref: '#/components/schemas/AgentEventStatus' }
        payload: { type: object }

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:    { type: string, format: email }
        password: { type: string }

    LoginResponse:
      type: object
      properties:
        token:
          type: string
          description: Signed JWT, valid for 24 hours
        user:
          $ref: '#/components/schemas/User'

    User:
      type: object
      properties:
        id:         { type: integer, format: int64 }
        email:      { type: string, format: email }
        name:       { type: string }
        role:       { type: string, enum: [admin, agent, viewer] }
        created_at: { type: string, format: date-time }

    HealthResponse:
      type: object
      properties:
        status:  { type: string, example: "ok" }
        version: { type: string, example: "0.1.0" }
        db:      { type: string, example: "ok" }

security:
  - BearerAuth: []

paths:
  /api/v1/health:
    get:
      tags: [meta]
      summary: Health check
      description: Returns service health, version, and DB connectivity. No auth required.
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HealthResponse' }

  /api/docs:
    get:
      tags: [meta]
      summary: OpenAPI spec
      description: Returns this OpenAPI 3.0 spec as YAML. No auth required.
      security: []
      responses:
        '200':
          description: OpenAPI YAML spec
          content:
            application/yaml:
              schema: { type: string }

  /api/v1/auth/login:
    post:
      tags: [auth]
      summary: Login
      description: Authenticates with email + password and returns a signed JWT.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LoginRequest' }
            example:
              email: admin@meadowlark.dev
              password: meadowlark-admin
      responses:
        '200':
          description: JWT issued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LoginResponse' }
        '400':
          description: Missing fields
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/auth/me:
    get:
      tags: [auth]
      summary: Current user
      description: Returns the profile of the authenticated user.
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/projects:
    get:
      tags: [projects]
      summary: List projects
      description: Returns all non-deleted projects. Filter by status or full-text search on name/address.
      parameters:
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/ProjectStatus' }
        - name: search
          in: query
          schema: { type: string }
          description: Case-insensitive search on name and address
      responses:
        '200':
          description: Array of projects
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Project' }
    post:
      tags: [projects]
      summary: Create project
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateProjectRequest' }
      responses:
        '201':
          description: Created project
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/v1/projects/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [projects]
      summary: Get project
      description: Returns a project with aggregated task summary counts.
      responses:
        '200':
          description: Project with task summary
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectWithSummary' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [projects]
      summary: Update project
      description: Partial update — only provided fields are changed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateProjectRequest' }
      responses:
        '200':
          description: Updated project
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [projects]
      summary: Delete project
      description: Soft-delete — sets deleted_at. The project will no longer appear in list queries.
      responses:
        '204':
          description: Deleted
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/contacts:
    get:
      tags: [contacts]
      summary: List contacts
      parameters:
        - name: type
          in: query
          schema: { $ref: '#/components/schemas/ContactType' }
        - name: search
          in: query
          schema: { type: string }
          description: Case-insensitive search on name and email
      responses:
        '200':
          description: Array of contacts
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Contact' }
    post:
      tags: [contacts]
      summary: Create contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateContactRequest' }
      responses:
        '201':
          description: Created contact
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/v1/contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [contacts]
      summary: Get contact
      responses:
        '200':
          description: Contact
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [contacts]
      summary: Update contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateContactRequest' }
      responses:
        '200':
          description: Updated contact
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/tasks:
    get:
      tags: [tasks]
      summary: List tasks
      parameters:
        - name: project_id
          in: query
          schema: { type: integer }
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/TaskStatus' }
        - name: priority
          in: query
          schema: { $ref: '#/components/schemas/TaskPriority' }
        - name: assignee_id
          in: query
          schema: { type: integer }
      responses:
        '200':
          description: Array of tasks
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Task' }
    post:
      tags: [tasks]
      summary: Create task
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateTaskRequest' }
      responses:
        '201':
          description: Created task
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/v1/tasks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer, format: int64 }
    get:
      tags: [tasks]
      summary: Get task
      responses:
        '200':
          description: Task
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [tasks]
      summary: Update task
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateTaskRequest' }
      responses:
        '200':
          description: Updated task
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [tasks]
      summary: Delete task
      description: Hard-deletes the task.
      responses:
        '204':
          description: Deleted
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/comms:
    get:
      tags: [comms]
      summary: List communication log entries
      description: Returns up to 200 entries, newest first. Agents use this to check prior contact.
      parameters:
        - name: project_id
          in: query
          schema: { type: integer }
        - name: channel
          in: query
          schema: { $ref: '#/components/schemas/CommChannel' }
        - name: direction
          in: query
          schema: { $ref: '#/components/schemas/CommDirection' }
      responses:
        '200':
          description: Array of comm entries
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Comm' }
    post:
      tags: [comms]
      summary: Ingest a communication
      description: |
        Used by agents to log inbound or outbound communications.
        Metadata can contain platform-specific IDs (e.g. Slack thread_ts, email message-id).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateCommRequest' }
            example:
              project_id: 1
              channel: email
              direction: out
              subject: "RFI #4 - Structural drawings"
              body: "Hi John, please find attached..."
              metadata:
                message_id: "<abc123@mail.meadowlark.dev>"
      responses:
        '201':
          description: Created comm entry
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comm' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/v1/agent-events:
    get:
      tags: [agent-events]
      summary: List recent agent events
      description: Returns the 500 most recent agent events, newest first.
      responses:
        '200':
          description: Array of agent events
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AgentEvent' }
    post:
      tags: [agent-events]
      summary: Record an agent action
      description: |
        Agents write every significant action here for auditability and observability.
        Write at the start with status=pending, then PATCH to done/failed on completion.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateAgentEventRequest' }
            example:
              agent_name: comms-agent
              action: email.sent
              entity_type: project
              entity_id: 42
              status: done
              payload:
                to: john@example.com
                subject: "RFI follow-up"
      responses:
        '201':
          description: Created agent event
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentEvent' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/v1/agent-events/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer, format: int64 }
    patch:
      tags: [agent-events]
      summary: Update agent event status
      description: Agents use this to mark an event done or failed after processing.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateAgentEventRequest' }
      responses:
        '200':
          description: Updated agent event
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentEvent' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/rfis:
    get:
      tags: [rfis]
      summary: List RFIs
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, schema: { type: integer } }
        - { name: status, in: query, schema: { type: string, enum: [draft, sent, responded] } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200':
          description: Paged list of RFIs
          content:
            application/json:
              schema:
                allOf:
                  - : '#/components/schemas/PagedResponse'
                  - properties:
                      data:
                        type: array
                        items: { : '#/components/schemas/RFI' }
    post:
      tags: [rfis]
      summary: Create RFI
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { : '#/components/schemas/CreateRFIRequest' }
      responses:
        '201':
          description: Created RFI
          content:
            application/json:
              schema: { : '#/components/schemas/RFI' }
        '400': { : '#/components/responses/BadRequest' }

  /api/v1/rfis/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [rfis]
      summary: Get RFI
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: RFI
          content:
            application/json:
              schema: { : '#/components/schemas/RFI' }
        '404': { : '#/components/responses/NotFound' }
    patch:
      tags: [rfis]
      summary: Update RFI
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { : '#/components/schemas/UpdateRFIRequest' }
      responses:
        '200':
          description: Updated RFI
          content:
            application/json:
              schema: { : '#/components/schemas/RFI' }
        '404': { : '#/components/responses/NotFound' }
    delete:
      tags: [rfis]
      summary: Delete RFI
      security: [{ BearerAuth: [] }]
      responses:
        '204': { description: Deleted }
        '404': { : '#/components/responses/NotFound' }

  /api/v1/purchase-orders:
    get:
      tags: [purchase-orders]
      summary: List purchase orders
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, schema: { type: integer } }
        - { name: status, in: query, schema: { type: string, enum: [draft, sent, approved, received] } }
        - { name: supplier_id, in: query, schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200':
          description: Paged list of purchase orders
          content:
            application/json:
              schema:
                allOf:
                  - : '#/components/schemas/PagedResponse'
                  - properties:
                      data:
                        type: array
                        items: { : '#/components/schemas/PurchaseOrder' }
    post:
      tags: [purchase-orders]
      summary: Create purchase order
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { : '#/components/schemas/CreatePORequest' }
      responses:
        '201':
          description: Created purchase order
          content:
            application/json:
              schema: { : '#/components/schemas/PurchaseOrder' }
        '400': { : '#/components/responses/BadRequest' }

  /api/v1/purchase-orders/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
    get:
      tags: [purchase-orders]
      summary: Get purchase order
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: Purchase order
          content:
            application/json:
              schema: { : '#/components/schemas/PurchaseOrder' }
        '404': { : '#/components/responses/NotFound' }
    patch:
      tags: [purchase-orders]
      summary: Update purchase order
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { : '#/components/schemas/UpdatePORequest' }
      responses:
        '200':
          description: Updated purchase order
          content:
            application/json:
              schema: { : '#/components/schemas/PurchaseOrder' }
        '404': { : '#/components/responses/NotFound' }
    delete:
      tags: [purchase-orders]
      summary: Delete purchase order
      security: [{ BearerAuth: [] }]
      responses:
        '204': { description: Deleted }
        '404': { : '#/components/responses/NotFound' }


components:
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    PagedResponse:
      type: object
      properties:
        data:
          type: array
          items: {}
        next_cursor:
          type: string
          description: Opaque base64 cursor; pass as ?cursor= to get next page
        has_more:
          type: boolean
      required: [data, next_cursor, has_more]

    RFI:
      type: object
      properties:
        id:          { type: integer, format: int64 }
        project_id:  { type: integer, format: int64 }
        subject:     { type: string }
        body:        { type: string }
        status:      { type: string, enum: [draft, sent, responded] }
        sent_at:     { type: string, format: date-time }
        responded_at:{ type: string, format: date-time }
        created_at:  { type: string, format: date-time }

    CreateRFIRequest:
      type: object
      required: [project_id, subject]
      properties:
        project_id: { type: integer, format: int64 }
        subject:    { type: string }
        body:       { type: string }
        status:     { type: string, enum: [draft, sent, responded] }

    UpdateRFIRequest:
      type: object
      properties:
        subject:      { type: string }
        body:         { type: string }
        status:       { type: string, enum: [draft, sent, responded] }
        responded_at: { type: string, format: date-time }

    PurchaseOrder:
      type: object
      properties:
        id:          { type: integer, format: int64 }
        project_id:  { type: integer, format: int64 }
        supplier_id: { type: integer, format: int64 }
        status:      { type: string, enum: [draft, sent, approved, received] }
        total:       { type: number, format: double }
        line_items:  { type: array, items: {} }
        created_at:  { type: string, format: date-time }

    CreatePORequest:
      type: object
      required: [project_id]
      properties:
        project_id:  { type: integer, format: int64 }
        supplier_id: { type: integer, format: int64 }
        status:      { type: string, enum: [draft, sent, approved, received] }
        total:       { type: number, format: double }
        line_items:  { type: array, items: {} }

    UpdatePORequest:
      type: object
      properties:
        status:      { type: string, enum: [draft, sent, approved, received] }
        total:       { type: number, format: double }
        line_items:  { type: array, items: {} }
        supplier_id: { type: integer, format: int64 }

