from __future__ import annotations

from datetime import date

from mcp.server.fastmcp import FastMCP

from .backend_client import BackendClient
from .session_store import SessionStore
from .tools.app_tools import AppTools
from .tools.auth_tools import AuthTools
from .tools.code_block_tools import CodeBlockTools
from .tools.feedback_tools import FeedbackTools
from .tools.export_tools import ExportTools
from .tools.file_tools import FileTools
from .tools.import_tools import ImportTools
from .tools.package_tools import PackageTools
from .tools.navigation_tools import NavigationTools
from .tools.directory_tools import DirectoryTools
from .tools.portal_tools import PortalTools
from .tools.qingbi_report_tools import QingbiReportTools
from .tools.role_tools import RoleTools
from .tools.solution_tools import SolutionTools
from .tools.task_context_tools import TaskContextTools
from .tools.view_tools import ViewTools
from .tools.workflow_tools import WorkflowTools
from .tools.workspace_tools import WorkspaceTools


def build_server() -> FastMCP:
    today = date.today()
    current_year = today.year
    server = FastMCP(
        "Qingflow MCP",
        instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.

## Authentication

Use `auth_use_credential` first when a local host such as createClaw can provide a credential. Treat the returned `wsId` and `qfVersion` as authoritative for the local session.
All resource tools operate with the logged-in user's Qingflow permissions.

## Shared Helper

`feedback_submit` is always available as a cross-cutting helper.

- Use it when the current MCP capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use.
- It does not require Qingflow login or workspace selection.
- Call it only after the user explicitly confirms submission.

## App Discovery

If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
Treat an explicit `view_id` as the exact frontend view context. If `system:all` fails, do not silently switch to `system:initiated`, `system:todo`, or another system view unless the user, frontend URL, or `app_get.accessible_views` explicitly selects that view.
If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.

## Schema-First Rule

Call `record_insert_schema_get` before `record_insert`.
For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
Prefer `record_code_block_schema_get` before `record_code_block_run` when field selection or binding diagnostics are unclear; if the exact code-block field id is already known from record/task detail, run directly.
Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.

- All `field_id` values must come from the schema response.
- Never guess field names or ids.

## Schema Scope

`record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
`record_update_schema_get` returns the current record's update-ready writable field set and route diagnostics. When `view_id` is explicit, it is the exact frontend view context and must not be silently replaced by another view.
`record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
`record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
`record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
`record_import_schema_get` returns import-ready column metadata.

- Hidden fields are omitted.
- Missing fields mean the field is not visible in the current permission scope.
- Read the top-level schema payload directly; do not guess missing writable fields.

## Analytics Path

`app_get -> record_browse_schema_get(view_id=...) -> record_access -> Python`

Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.

Use `record_access` to write local CSV shard files for analysis, then use Python to compute counts, rankings, ratios, trends, and final conclusions.

Use this DSL shape:

- `columns`: `[{{field_id}}]`
- `where`: `[{{field_id, op, value}}]`
- `order_by`: `[{{field_id, direction}}]`

Important key rules:

- Use `op`
- Do **not** use `type`
- Do **not** use `agg`
- Do **not** use `aggregation`
- Do **not** use `operator`

Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.

## Record CRUD Path

`app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
`record_insert_schema_get -> record_insert(items)`
`record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
`record_list / record_get -> record_delete(system view_id)`
`record_code_block_run` directly when the exact code-block field is known; otherwise `record_code_block_schema_get -> record_code_block_run`

- Use `columns` as `[{{field_id}}]`
- Use `record_list(query=..., query_fields=[{{field_id}}])` for fuzzy single-record lookup, then follow `lookup.next_action`; `query_fields` is search scope and `columns` is display shape.
- Use `where` items as `{{field_id, op, value}}`
- Use `order_by` items as `{{field_id, direction}}`
- Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL

- `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
- `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. On success, read `status`, `update_route`, and `verification_status`; on failure, read the failure reason and route diagnostics.
- For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
- For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
- Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
- `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
- `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
- `record_update_schema_get` exposes the writable field set and route candidates for the record; with an explicit `view_id`, diagnostics stay scoped to that selected frontend view. Not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
- `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
- `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
- Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
- Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
- When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.

- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
- Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
- CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback; pass `--record-id`, `--workflow-node-id`, and `--fields-file` only when the candidate scope must match an existing runtime context.
- For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.

## Code Block Path

Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.

- Prefer resolving the exact code-block field from `record_code_block_schema_get`, but do not treat applicant-schema 40002 as final denial when record/task detail already exposes the code-block field id.
- `record_code_block_run` uses the record/task detail answers for the execution context; when applicant schema is unavailable it can still execute with a numeric code-block field id, but schema-bound relation writeback may be skipped.
- Treat code-block execution as write-capable, not read-only.
- If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
- For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
- In workflow context, pass `role=3` and the exact `workflow_node_id`.
- After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.

## Import Path

`app_get -> record_import_schema_get -> record_import_template_get -> record_import_verify -> (optional authorized record_import_repair_local) -> record_import_start -> record_import_status_get`

- Check `app_get.data.import_capability` before doing import work.
- If `import_capability.can_import=false`, stop before template download, file repair, or import start.
- Import must go through `verify -> start`; do not start directly from a raw file path.
- `record_import_start` requires an explicit `being_enter_auditing` choice. Do not assume a default.
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.

## Export Path

`view_get -> record_export_start -> record_export_status_get -> record_export_get`

- `record_export_direct` is the one-shot export path that starts the export, waits for completion, downloads locally, and still returns remote download links.
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list` (`system:*` and `custom:*`).
- `record_export_start` / `record_export_direct` support frontend-like row selection:
  - omit `record_ids` to export all rows in the selected view
  - pass `record_ids` to export selected rows only
- `record_export_start` / `record_export_direct` also support internal query selection:
  - pass `where` to resolve matching `record_id` values first
  - pass `order_by` to keep the internal query and export row order aligned with `record_list`
  - then run native export as selected rows
  - `where/order_by` and `record_ids` are mutually exclusive
- `record_export_start` / `record_export_direct` also support frontend-like column selection:
  - omit `columns` to export all current-view fields
  - pass `columns` to export only selected fields, preserving the provided order
- `include_workflow_log=true` maps to the native workflow-log export switch.

## Task Workflow Path

`task_list -> task_get -> task_action_execute`

- `task_list` returns task-card summaries keyed by `task_id`.
- For detail reads and actions, pass the exact `task_id` from `task_list.data.items[].task_id`.
- `task_id` is not a row number, list index, record id, or workflow node id.
- `task_action_execute` only uses `task_id`; do not reconstruct or pass `app_key + record_id + workflow_node_id` for actions.
- `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
- Use `task_associated_report_detail_get` for associated view or report details.
- Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, first choose an accessible view with `app_get`, then call `record_logs_get(app_key, record_id, view_id)` with that same explicit `view_id`.

## Time Handling

Normalize relative dates before building DSL.

- If the user says `3月` without a year, use the current year: `{current_year}`
- Convert month-only phrases into explicit legal date ranges
- Never send impossible dates such as `2026-02-29`

## Environment

Default to `prod` unless the user explicitly specifies `test`.

## Constraints

Avoid builder-side app or schema changes here.

## Feedback Path

If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.

- First summarize what is still not working
- Ask the user whether to submit feedback
- Call `feedback_submit` only after explicit user confirmation""",
    )
    sessions = SessionStore()
    backend = BackendClient()
    AuthTools(sessions, backend).register(server)
    FeedbackTools(backend, mcp_side="通用").register(server)
    WorkspaceTools(sessions, backend).register(server)
    FileTools(sessions, backend).register(server)
    ImportTools(sessions, backend).register(server)
    ExportTools(sessions, backend).register(server)
    CodeBlockTools(sessions, backend).register(server)
    TaskContextTools(sessions, backend).register(server)
    RoleTools(sessions, backend).register(server)
    AppTools(sessions, backend).register(server)
    QingbiReportTools(sessions, backend).register(server)
    PackageTools(sessions, backend).register(server)
    NavigationTools(sessions, backend).register(server)
    PortalTools(sessions, backend).register(server)
    DirectoryTools(sessions, backend).register(server)
    WorkflowTools(sessions, backend).register(server)
    ViewTools(sessions, backend).register(server)
    SolutionTools(sessions, backend).register(server)
    return server


mcp = build_server()


def main() -> None:
    mcp.run()


if __name__ == "__main__":
    main()
