"""Test API routes."""
from datetime import UTC, date, datetime, time
from flask import Blueprint, jsonify, request, g
from werkzeug.utils import secure_filename
from pydantic import ValidationError

from app.functionalities.ml_search.ml_search import InvalidSearchQuery, MLSearch
from app.functionalities.OCR.pdf_parser import parse_pdf_bytes
from app.models.test import Test
from app.models.hospital import Hospital
from app.repositories import test_repository, hospital_repository
from app.routes.access import ensure_can_access_patient_data, ensure_can_create_for_patient
from app.routes.auth import auth_required


tests_bp = Blueprint('tests_api', __name__)
ml_search = MLSearch()


def _parse_test_date_param(param_name: str, raw_value: str, *, end_of_day: bool = False) -> datetime:
    """Parse a test date query parameter into a timezone-aware UTC datetime."""
    value = raw_value.strip()
    if not value:
        raise ValueError(f"{param_name} cannot be empty")

    try:
        day = date.fromisoformat(value)
        local_time = time.max if end_of_day else time.min
        return datetime.combine(day, local_time, tzinfo=UTC)
    except ValueError:
        pass

    try:
        dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
    except ValueError as exc:
        raise ValueError(f"{param_name} must be ISO-8601 date or datetime") from exc

    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt.astimezone(UTC)


@tests_bp.route('/tests/search/parse', methods=['POST'])
@auth_required
def parse_test_search_query_endpoint():
    """Parse a natural-language bloodwork search query.
    ---
    tags:
        - Tests
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/TestSearchParseRequest'
    responses:
        200:
            description: Parsed query payload
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestSearchParseResponse'
                    example:
                        raw_query: show glucose from January 1st 2024 to March 1st 2024
                        test_queries:
                            - test_parameter: glucose
                              start: '2024-01-01'
                              end: '2024-03-01'
                              text: january 1st 2024 to march 1st 2024
                        out_of_reference_only: false
        400:
            description: Missing or invalid query
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
                    examples:
                        missing-query:
                            summary: Missing query field
                            value:
                                error: MissingField
                                field: query
                        invalid-query:
                            summary: Query without parseable dates
                            value:
                                error: InvalidSearchQuery
                                detail: No dates found in query. Please specify a date or date range.
    """
    payload = request.get_json(silent=True) or {}
    query = str(payload.get('query', '')).strip()
    if not query:
        return jsonify({'error': 'MissingField', 'field': 'query'}), 400

    patient_id = str(payload.get('patientId', '')).strip()
    if not patient_id:
        return jsonify({'error': 'MissingField', 'field': 'patientId'}), 400

    _, err = ensure_can_access_patient_data(patient_id)
    if err:
        return err

    try:
        parsed = ml_search.search(query, patient_id)
    except InvalidSearchQuery as exc:
        return jsonify({'error': 'InvalidSearchQuery', 'detail': str(exc)}), 400

    return jsonify(parsed.model_dump(mode='json'))


@tests_bp.route('/tests', methods=['POST'])
@auth_required
def create_test_endpoint():
    """Create a test record.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/TestCreateRequest'
    responses:
        201:
            description: Test created
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponse'
        400:
            description: Validation or payload error
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    payload = request.get_json(silent=True) or {}
    patient_id = str(payload.get('patientId', ''))
    create_err = ensure_can_create_for_patient(patient_id)
    if create_err:
        return create_err

    try:
        test = Test.model_validate(payload)
    except ValidationError as exc:
        return jsonify({'error': 'ValidationError', 'detail': str(exc)}), 400

    created = test_repository.create(test)
    return jsonify(created.model_dump(by_alias=True, mode='json')), 201


@tests_bp.route('/tests/ocr', methods=['POST'])
@auth_required
def upload_ocr_test_endpoint():
    """Upload a PDF and return parsed blood-work results as JSON.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    requestBody:
        required: true
        content:
            multipart/form-data:
                schema:
                    type: object
                    properties:
                        file:
                            type: string
                            format: binary
    responses:
        200:
            description: PDF parsed successfully and structured JSON returned
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponse'
        400:
            description: Bad request (missing file or not a PDF)
        500:
            description: Server error during parsing
    """
    if 'file' not in request.files:
        return jsonify({'error': 'No file part in request'}), 400

    file = request.files['file']
    if file.filename == '' or not file.filename:
        return jsonify({'error': 'No selected file'}), 400

    try:
        result = parse_pdf_bytes(file.stream, filename=file.filename)
        return jsonify(result), 200
    except Exception as e:
        return jsonify({'error': 'PDF parsing failed', 'detail': str(e)}), 500



@tests_bp.route('/tests/json', methods=['POST'])
@auth_required
def ingest_json_test_endpoint():
    """Submit a JSON payload to save as a test in the database.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/TestCreateRequest'
    responses:
        201:
            description: Test newly created
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponse'
        400:
            description: Validation error
        403:
            description: Forbidden to create test for patient
    """
    payload = request.get_json(silent=True) or {}
    
    auth_user = g.get('auth_user') or {}
    patient_id = auth_user.get('sub')
    
    if not patient_id:
        return jsonify({'error': 'Unauthorized', 'detail': 'No user subject found in token'}), 401
    
    # We will still ensure they can create for this patient, which is a bit redundant if they are posting to themselves,
    # but handles edges cases correctly.
    create_err = ensure_can_create_for_patient(patient_id)
    if create_err:
        return create_err
        
    try:
        # Check if the hospital exists, if not create it
        hospital_name = payload.get('hospitalName', 'Unknown Hospital')
        hospital = hospital_repository.find_by_tracking_id(hospital_name)
        if not hospital:
            hospital = Hospital(
                trackingId=hospital_name,
                name=hospital_name,
                address="Unknown Address",
            )
            hospital = hospital_repository.create(hospital)
        
        # Map the payload to the Test model
        # The parsed OCR payload usually has: date, hospitalName, patientId, testId, subtests
        test_data = {
            "testId": payload.get("testId", "unknown"),
            "sutCode": payload.get("sutCode", "UNKNOWN"),
            "name": payload.get("testName") or "Blood Work (OCR)",
            "patientId": patient_id,
            "subtests": payload.get("subtests", [])
        }
        
        # If the date is passed, include it
        if "date" in payload and payload["date"]:
            test_data["date"] = payload["date"]
            
        test = Test.model_validate(test_data)
    except ValidationError as exc:
        return jsonify({'error': 'ValidationError', 'detail': str(exc)}), 400
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

    created = test_repository.create(test)
    return jsonify(created.model_dump(by_alias=True, mode='json')), 201



@tests_bp.route('/tests/<test_id>', methods=['GET'])
@auth_required
def get_test_by_id_endpoint(test_id: str):
    """Get a test by ID.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    parameters:
        - in: path
          name: test_id
          required: true
          schema:
            type: string
    responses:
        200:
            description: Test found
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        404:
            description: Test not found
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    test = test_repository.find_by_id(test_id)
    if not test:
        return jsonify({'error': 'NotFound'}), 404

    _, err = ensure_can_access_patient_data(str(test.patientId))
    if err:
        return err

    return jsonify(test.model_dump(by_alias=True, mode='json'))


@tests_bp.route('/tests', methods=['GET'])
@auth_required
def list_tests_by_patient_endpoint():
    """List tests by patient ID query parameter.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    parameters:
        - in: query
          name: patientId
          required: true
          schema:
            type: string
        - in: query
          name: startDate
          required: false
          schema:
            type: string
            format: date
          description: Inclusive lower bound for test date (ISO-8601 date or datetime)
        - in: query
          name: endDate
          required: false
          schema:
            type: string
            format: date
          description: Inclusive upper bound for test date (ISO-8601 date or datetime)
    responses:
        200:
            description: Test list
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponseList'
        400:
            description: Missing or invalid patientId
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    patient_id = request.args.get('patientId')
    if not patient_id:
        return jsonify({'error': 'MissingQueryParam', 'param': 'patientId'}), 400

    start_raw = request.args.get('startDate') or request.args.get('start')
    end_raw = request.args.get('endDate') or request.args.get('end')

    start_date = None
    end_date = None
    if start_raw is not None:
        try:
            start_date = _parse_test_date_param('startDate', start_raw, end_of_day=False)
        except ValueError as exc:
            return jsonify({'error': 'InvalidDateQueryParam', 'param': 'startDate', 'detail': str(exc)}), 400

    if end_raw is not None:
        try:
            end_date = _parse_test_date_param('endDate', end_raw, end_of_day=True)
        except ValueError as exc:
            return jsonify({'error': 'InvalidDateQueryParam', 'param': 'endDate', 'detail': str(exc)}), 400

    if start_date and end_date and start_date > end_date:
        return jsonify({'error': 'InvalidDateRange', 'detail': 'startDate must be before or equal to endDate'}), 400

    _, err = ensure_can_access_patient_data(patient_id)
    if err:
        return err

    try:
        tests = test_repository.find_by_patient_id(patient_id, start_date=start_date, end_date=end_date)
    except ValueError as exc:
        return jsonify({'error': 'InvalidPatientId', 'detail': str(exc)}), 400

    return jsonify([t.model_dump(by_alias=True, mode='json') for t in tests])


@tests_bp.route('/tests/<test_id>', methods=['PATCH'])
@auth_required
def update_test_endpoint(test_id: str):
    """Update a test record.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    parameters:
        - in: path
          name: test_id
          required: true
          schema:
            type: string
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/TestUpdateRequest'
    responses:
        200:
            description: Test updated
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/TestResponse'
        400:
            description: No updatable fields or invalid ID
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        404:
            description: Test not found
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    existing = test_repository.find_by_id(test_id)
    if not existing:
        return jsonify({'error': 'NotFound'}), 404

    _, err = ensure_can_access_patient_data(str(existing.patientId))
    if err:
        return err

    updates = request.get_json(silent=True) or {}
    forbidden = {'_id', 'id', 'created_at', 'updated_at'}
    filtered_updates = {k: v for k, v in updates.items() if k not in forbidden}
    if not filtered_updates:
        return jsonify({'error': 'NoUpdatableFields'}), 400

    try:
        updated = test_repository.update(test_id, filtered_updates)
    except ValueError as exc:
        return jsonify({'error': 'InvalidTestId', 'detail': str(exc)}), 400

    if not updated:
        return jsonify({'error': 'NotFound'}), 404

    return jsonify(updated.model_dump(by_alias=True, mode='json'))


@tests_bp.route('/tests/<test_id>', methods=['DELETE'])
@auth_required
def delete_test_endpoint(test_id: str):
    """Delete a single test record.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    parameters:
        - in: path
          name: test_id
          required: true
          schema:
            type: string
    responses:
        200:
            description: Test deleted
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/DeleteResponse'
        400:
            description: Invalid test ID
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        404:
            description: Test not found
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    existing = test_repository.find_by_id(test_id)
    if not existing:
        return jsonify({'error': 'NotFound'}), 404

    _, err = ensure_can_access_patient_data(str(existing.patientId))
    if err:
        return err

    auth_user = g.get('auth_user') or {}
    requester_id = str(auth_user.get('sub', ''))
    requester_roles = set(auth_user.get('roles', []))
    if 'admin' not in requester_roles and requester_id != str(existing.patientId):
        return jsonify({'error': 'Forbidden', 'detail': 'Only the patient can delete this test'}), 403

    try:
        deleted = test_repository.delete_by_id(test_id)
    except ValueError as exc:
        return jsonify({'error': 'InvalidTestId', 'detail': str(exc)}), 400

    if not deleted:
        return jsonify({'error': 'NotFound'}), 404

    return jsonify({'ok': True})


@tests_bp.route('/tests', methods=['DELETE'])
@auth_required
def delete_tests_by_patient_endpoint():
    """Delete all tests for a patient.
    ---
    tags:
        - Tests
    security:
        - BearerAuth: []
    parameters:
        - in: query
          name: patientId
          required: true
          schema:
            type: string
          description: Patient ID whose tests should be deleted
    responses:
        200:
            description: Tests deleted
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/BulkDeleteResponse'
        400:
            description: Missing or invalid patient ID
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Unauthorized
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Forbidden
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    patient_id = request.args.get('patientId', '')
    if not patient_id:
        return jsonify({'error': 'MissingQueryParam', 'param': 'patientId'}), 400

    _, err = ensure_can_access_patient_data(patient_id)
    if err:
        return err

    auth_user = g.get('auth_user') or {}
    requester_id = str(auth_user.get('sub', ''))
    requester_roles = set(auth_user.get('roles', []))
    if 'admin' not in requester_roles and requester_id != str(patient_id):
        return jsonify({'error': 'Forbidden', 'detail': 'Only the patient can delete tests'}), 403

    try:
        deleted_count = test_repository.delete_by_patient_id(patient_id)
    except ValueError as exc:
        return jsonify({'error': 'InvalidPatientId', 'detail': str(exc)}), 400

    return jsonify({'ok': True, 'deletedCount': deleted_count})
