"""Typed, exact-actor device note receipts. No generic replay or new storage schema.

The encryption key is derived server-side from the existing salted scrypt secret,
with independent HMAC domains. It never authenticates a request. Password changes,
membership changes and recovery intentionally make old device ciphertext locked.
"""
import base64
from contextlib import contextmanager
import hashlib
import hmac
import json
import time
from uuid import UUID
from fastapi import HTTPException, Request
from pydantic import Field, field_validator
from .models import Strict
from .field_models import LogFields, LogEdit
from .fieldwork import access, log_event, log_projection, update_log
from .customer_review import active_access
from .store import one, uid, now, digest


class Device(Strict):
    device_id: UUID


class Binding(Device):
    scope: str = Field(pattern=r'^[a-f0-9]{64}$')
    job_version: int = Field(strict=True, ge=1)


class Inspect(Strict):
    binding: Binding


class Revise(Inspect):
    revision: int = Field(strict=True, ge=2, le=1000)


class Deliver(Inspect):
    revision: int = Field(strict=True, ge=1, le=1000)
    content: str = Field(min_length=1, max_length=3000)
    work_date: str

    @field_validator("work_date")
    @classmethod
    def valid_date(cls, value):
        from .workforce import work_date
        return work_date(value)


class Correction(Inspect):
    edit_id: UUID
    version: int = Field(strict=True, ge=1)
    content: str = Field(min_length=1, max_length=3000)
    work_date: str
    reason: str = Field(default='', max_length=500)

    @field_validator('work_date')
    @classmethod
    def valid_date(cls, value):
        from .workforce import work_date
        return work_date(value)


class ResolveCorrection(Inspect):
    edit_id: UUID


def fail(status, message):
    raise HTTPException(status, message)


def packed(value):
    return json.dumps(value, sort_keys=True, separators=(',', ':'))


def mount_device_notes(app, store, session):
    @contextmanager
    def current(request, job_id, device_id):
        # Keep membership/session mutations and exact-job mutations behind the
        # same lock ordering throughout permission check, receipt and log commit.
        with store.registry() as registry:
            registry.execute('BEGIN IMMEDIATE')
            s = session(request, csrf=True)
            secret = one(registry, 'SELECT password FROM users WHERE id=?', (s['subject'],))['password']
            dataset = one(registry, 'SELECT dataset_id FROM dataset_identity')['dataset_id']
            with store.tenant(s['business_id']) as db:
                db.execute('BEGIN IMMEDIATE')
                j = access(db, s, job_id, active=True)
                j['version'] = one(db, 'SELECT version FROM jobs WHERE id=?', (job_id,))['version']
                workflow = one(db, 'SELECT content FROM job_workflow WHERE job_id=?', (job_id,))
                if not active_access(db, j['customer_id']) or (workflow and json.loads(workflow['content'])['archived']):
                    fail(404, 'This job is no longer available for device notes.')
                from .customer_moves import epoch
                identity_parts = [dataset, s['business_id'], s['subject'], s['membership_id'], s['membership_version'], job_id, j['customer_id'], str(device_id)]
                ownership_epoch = epoch(db, job_id)
                if ownership_epoch: identity_parts.append(ownership_epoch)
                identity = packed(identity_parts)
                def derive(domain):
                    return hmac.new(secret.encode(), (domain + identity).encode(), hashlib.sha256).digest()
                scope = derive('shs-device-note-scope-v1:').hex()
                key = derive('shs-device-note-key-v1:')
                yield db, s, j, scope, key

    def bound(binding, scope):
        if not hmac.compare_digest(binding.scope, scope):
            fail(403, 'This retained note belongs to different or expired access. Its encrypted device copy is kept locked.')

    def receipt(db, operation, scope):
        fingerprint = 'device-note-v1:' + digest(scope + str(operation))
        row = one(db, 'SELECT * FROM operations WHERE key=?', (str(operation),))
        closed = 'device-note-closed-v2:' + digest(scope + str(operation))
        if row and row['fingerprint'] not in (fingerprint, closed):
            fail(409, 'This operation belongs to another note. It cannot be reused.')
        return fingerprint, json.loads(row['result']) if row else None

    def save(db, operation, fingerprint, value):
        db.execute('INSERT INTO operations VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET fingerprint=excluded.fingerprint,result=excluded.result', (str(operation), fingerprint, packed(value)))

    def confirmed(db, s, job_id, operation, scope):
        _, prior = receipt(db, operation, scope)
        if not prior or prior['state'] != 'confirmed':
            fail(409, 'Resolve this note before reviewing a saved correction.')
        row = one(db, 'SELECT * FROM field_logs WHERE id=? AND job_id=?', (prior['id'], job_id))
        if not row:
            fail(404, 'The exact saved log is no longer available.')
        return prior, row

    def edit_receipt(db, operation, edit_id, scope):
        fingerprint = 'device-note-edit-v1:' + digest(scope + str(operation) + str(edit_id))
        row = one(db, 'SELECT * FROM operations WHERE key=?', (str(edit_id),))
        if row and row['fingerprint'] != fingerprint:
            fail(409, 'This correction identity belongs to another operation.')
        return fingerprint, json.loads(row['result']) if row else None

    @app.post('/api/field/jobs/{job_id}/note-access')
    def unlock(job_id: str, payload: Device, request: Request):
        with current(request, job_id, payload.device_id) as (_, s, j, scope, key):
            return {'binding': {'scope': scope, 'device_id': str(payload.device_id), 'job_version': j['version']},
                    'key': base64.b64encode(key).decode(), 'lease_ms': max(0, min(120000, int((s['expires'] - time.time()) * 1000)))}

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/inspect')
    def inspect(job_id: str, operation: UUID, payload: Inspect, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, _, _, scope, _):
            bound(payload.binding, scope)
            _, prior = receipt(db, operation, scope)
            return prior or {'state': 'never-sent', 'revision': 1}

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/cancel')
    def cancel(job_id: str, operation: UUID, payload: Inspect, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, _, _, scope, _):
            bound(payload.binding, scope)
            _, prior = receipt(db, operation, scope)
            if prior and prior['state'] in ('confirmed', 'cancelled'):
                return prior
            result = {'state': 'cancelled', 'revision': prior['revision'] if prior else 1}
            # A distinct fingerprint also makes v0.22 refuse this terminal row.
            save(db, operation, 'device-note-closed-v2:' + digest(scope + str(operation)), result)
            return result

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/saved')
    def saved(job_id: str, operation: UUID, payload: Inspect, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, s, _, scope, _):
            bound(payload.binding, scope)
            prior, row = confirmed(db, s, job_id, operation, scope)
            if row['archived'] and s['role'] != 'owner':
                fail(404, 'This saved log is no longer available to you.')
            return {'receipt': prior, 'log': log_projection(db, row, s['role'] == 'owner', s)}

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/apply-correction')
    def apply_correction(job_id: str, operation: UUID, payload: Correction, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, s, _, scope, _):
            bound(payload.binding, scope)
            _, row = confirmed(db, s, job_id, operation, scope)
            if not log_projection(db, row, s['role'] == 'owner', s)['editable']:
                fail(403, 'This log is no longer editable. Keep or discard the device proposal after review.')
            fingerprint, prior = edit_receipt(db, operation, payload.edit_id, scope)
            body_hash = digest(packed(payload.model_dump(mode='json')))
            if prior:
                if prior['state'] == 'cancelled':
                    fail(409, 'This correction attempt was cancelled. Review before starting another.')
                if prior['payload_hash'] != body_hash:
                    fail(409, 'This correction identity has different text. Resolve the original attempt first.')
                return prior
            if row['kind'] != 'note':
                fail(409, 'This saved record is no longer a text note. Review it in the normal field workflow.')
            edit = LogEdit(version=payload.version, content=payload.content, work_date=payload.work_date, reason=payload.reason, photo_ids=json.loads(row['photo_ids']), quantity=row['quantity'], unit=row['unit'])
            result = update_log(db, s, job_id, row['id'], edit)
            result = {'state': 'applied', 'id': row['id'], 'version': result['version'], 'payload_hash': body_hash}
            save(db, payload.edit_id, fingerprint, result)
            return result

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/resolve-correction')
    def resolve_correction(job_id: str, operation: UUID, payload: ResolveCorrection, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, s, _, scope, _):
            bound(payload.binding, scope)
            confirmed(db, s, job_id, operation, scope)
            fingerprint, prior = edit_receipt(db, operation, payload.edit_id, scope)
            if prior:
                return prior
            result = {'state': 'cancelled'}
            save(db, payload.edit_id, fingerprint, result)
            return result

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/revise')
    def revise(job_id: str, operation: UUID, payload: Revise, request: Request):
        with current(request, job_id, payload.binding.device_id) as (db, _, _, scope, _):
            bound(payload.binding, scope)
            fingerprint, prior = receipt(db, operation, scope)
            if prior and prior['state'] == 'cancelled':
                fail(409, 'This note was cancelled; a delayed revision was refused.')
            if prior and prior['state'] == 'confirmed':
                return prior
            revision = prior['revision'] if prior else 1
            if payload.revision == revision and prior:
                return prior
            if payload.revision != revision + 1:
                fail(409, 'Another tab revised this note. Reopen the retained copy.')
            # A durable fence rejects a delayed delivery of the previous revision.
            result = {'state': 'never-sent', 'revision': payload.revision}
            save(db, operation, fingerprint, result)
            return result

    @app.post('/api/field/jobs/{job_id}/device-notes/{operation}/deliver')
    def deliver(job_id: str, operation: UUID, payload: Deliver, request: Request):
        fields = LogFields(content=payload.content, work_date=payload.work_date)
        with current(request, job_id, payload.binding.device_id) as (db, s, j, scope, _):
            bound(payload.binding, scope)
            fingerprint, prior = receipt(db, operation, scope)
            if prior and prior['state'] == 'cancelled':
                fail(409, 'This note was cancelled; a delayed delivery was refused.')
            body_hash = digest(packed(payload.model_dump(mode='json')))
            if prior and prior['state'] == 'confirmed':
                if prior['payload_hash'] != body_hash:
                    fail(409, 'An earlier version is already saved. Check its confirmation before changing anything.')
                return prior
            if payload.revision != (prior['revision'] if prior else 1):
                fail(409, 'This note was revised. A stale delivery was refused.')
            if payload.binding.job_version != j['version']:
                fail(409, 'The job changed. Review and retain a correction before sending this note.')
            if db.execute('SELECT COUNT(*) FROM field_logs').fetchone()[0] >= 5000:
                fail(409, 'The local field log limit has been reached.')
            lid = uid()
            db.execute('INSERT INTO field_logs(id,job_id,customer_id,author_id,membership_id,kind,content,work_date,quantity,unit,photo_ids,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)',
                       (lid, job_id, j['customer_id'], s['subject'], s['membership_id'], 'note', fields.content, fields.work_date, '1', 'each', '[]', now(), now()))
            log_event(db, lid, 'created', s)
            result = {'state': 'confirmed', 'revision': payload.revision, 'id': lid, 'version': 1, 'payload_hash': body_hash}
            save(db, operation, fingerprint, result)
            return result
