"""One bounded anonymous submission; no upload tokens or public media URLs."""
import base64
import binascii
from datetime import date, datetime
import hashlib
import json
import os
import re
import uuid
from zoneinfo import ZoneInfo

from fastapi import HTTPException
from pydantic import Field, field_validator

from .models import Enquiry, Strict
from .photos import photo_snapshot, MAX_BUSINESS_PHOTOS
from .enquiry_images import decode_enquiry_image
from .enquiry_image_worker import MAX_FILE_BYTES, MAX_OUTPUT_BYTES
from .store import uid, one, many, audit

MAX_PHOTOS = 5
MAX_TOTAL_BYTES = MAX_PHOTOS * MAX_FILE_BYTES
# Five full-size originals plus base64 expansion, filenames and form JSON.
MAX_REQUEST_BYTES = 70 * 1024 * 1024
MAX_NORMALIZED_BYTES = MAX_PHOTOS * MAX_OUTPUT_BYTES
MAX_BUSINESS_BYTES = 128 * 1024 * 1024
TABLES = ('enquiry_photo_sources', 'enquiry_intakes')
PUBLIC_ACTOR = 'public-enquiry'
PHOTO_ACTOR = {'subject': PUBLIC_ACTOR, 'membership_id': ''}
SCHEMA = '''
CREATE TABLE IF NOT EXISTS enquiry_intakes(
 job_id TEXT PRIMARY KEY REFERENCES jobs(id),customer_id TEXT NOT NULL REFERENCES customers(id),
 submitted TEXT NOT NULL,operation_key TEXT UNIQUE NOT NULL,content_sha256 TEXT NOT NULL,created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS enquiry_photo_sources(
 photo_id TEXT PRIMARY KEY REFERENCES job_photos(id),job_id TEXT NOT NULL REFERENCES enquiry_intakes(job_id),
 position INTEGER NOT NULL CHECK(position BETWEEN 0 AND 4),filename TEXT NOT NULL,source_format TEXT NOT NULL,
 source_bytes INTEGER NOT NULL,source_sha256 TEXT NOT NULL,created_at TEXT NOT NULL,UNIQUE(job_id,position));
'''


class EnquiryFacts(Enquiry):
    preferred_date: str = Field(default='', max_length=10)

    @field_validator('preferred_date', mode='before')
    @classmethod
    def blank_date(cls, value):
        return '' if value is None else value

    @field_validator('preferred_date')
    @classmethod
    def calendar_day(cls, value):
        if value:
            if not re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}', value):
                raise ValueError('Use a valid calendar date, or leave the preferred date blank.')
            try:
                if date.fromisoformat(value).isoformat() != value: raise ValueError()
            except ValueError:
                raise ValueError('Use a valid calendar date, or leave the preferred date blank.')
        return value


class EnquiryPhoto(Strict):
    filename: str = Field(min_length=1, max_length=120)
    data: str = Field(min_length=4, max_length=4 * ((MAX_FILE_BYTES + 2) // 3))

    @field_validator('filename')
    @classmethod
    def filename_shape(cls, value):
        if value in ('.', '..') or any(c in value for c in '/\\') or any(ord(c) < 32 or ord(c) == 127 for c in value):
            raise ValueError('Choose a file with a simple filename, without folders or control characters.')
        return value


class PublicEnquiry(EnquiryFacts):
    photos: list[EnquiryPhoto] = Field(default_factory=list, max_length=MAX_PHOTOS)


def uk_today():
    return datetime.now(ZoneInfo('Europe/London')).date().isoformat()


def content_digest(facts, photos):
    return hashlib.sha256(json.dumps({'facts':facts,'photos':photos},sort_keys=True).encode()).hexdigest()


def prepare_photos(photos):
    """Validate all files before opening a transaction or creating anything."""
    prepared = []
    total = normalized = 0
    for index, photo in enumerate(photos):
        prefix = f'Photo {index + 1} ({photo.filename}): '
        try:
            try:
                raw = base64.b64decode(photo.data, validate=True)
            except (ValueError, binascii.Error):
                raise HTTPException(422, 'the file could not be read. Remove it and select it again.')
            if not raw or len(raw) > MAX_FILE_BYTES:
                raise HTTPException(413, 'choose a PNG, JPEG, WebP or HEIC/HEIF no larger than 10 MB (10,485,760 bytes).')
            total += len(raw)
            if total > MAX_TOTAL_BYTES:
                raise HTTPException(413, 'the combined original files must be no larger than 50 MB.')
            body, width, height, source_format = decode_enquiry_image(raw)
            normalized += len(body)
            if normalized > MAX_NORMALIZED_BYTES:
                raise HTTPException(413, 'these images are too large after safe conversion. Choose smaller images (65 MB combined after conversion).')
            prepared.append({'filename': photo.filename, 'source_format': source_format,
                'source_bytes': len(raw), 'source_sha256': hashlib.sha256(raw).hexdigest(),
                'body': body, 'sha256': hashlib.sha256(body).hexdigest(), 'width': width, 'height': height})
        except HTTPException as error:
            raise HTTPException(error.status_code, prefix + error.detail) from error
    return prepared


def write_photo(store, business_id, blob_id, body, created):
    path = store.photo_path(business_id, blob_id)
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
    created.append(path)
    with os.fdopen(descriptor, 'wb') as stream:
        stream.write(body)
        stream.flush()
        os.fsync(stream.fileno())


def submit(store, business_id, payload, request, once, add_enquiry):
    prepared = prepare_photos(payload.photos)
    facts = EnquiryFacts.model_validate(payload.model_dump(exclude={'photos'})).model_dump()
    # No encoded images in retry records. Original bytes, names and order remain
    # part of the fingerprint, scoped to this exact business and public route.
    fingerprint = {'facts': facts, 'photos': [{k: p[k] for k in ('filename', 'source_bytes', 'source_sha256')} for p in prepared]}
    content_hash = content_digest(facts, fingerprint['photos'])
    created = []
    try:
        with store.tenant(business_id) as db:
            def action():
                operation_key = str(uuid.UUID(request.headers['idempotency-key']))
                original = one(db, 'SELECT * FROM enquiry_intakes WHERE operation_key=?', (operation_key,))
                if original:
                    if original['content_sha256'] != content_hash:
                        raise HTTPException(409, 'That submission ID belongs to different enquiry details. No new enquiry was saved.')
                    return {'id': original['job_id'], 'customer_id': original['customer_id']}
                # Evaluate relative dates only for a new submission. An exact
                # successful retry still works after midnight or later days.
                if facts['preferred_date'] and facts['preferred_date'] < uk_today():
                    raise HTTPException(422, 'Preferred date: choose today or a later UK calendar date, or leave it blank. This is a preference, not a booking.')
                count, size = db.execute('SELECT COUNT(*),COALESCE(SUM(bytes),0) FROM photo_blobs').fetchone()
                if prepared and (count + len(prepared) > MAX_BUSINESS_PHOTOS or size + sum(len(p['body']) for p in prepared) > MAX_BUSINESS_BYTES):
                    raise HTTPException(409, 'This local business has reached its private photo allowance. No enquiry was saved. Try without photos or contact the business.')
                result = add_enquiry(db, payload, 'website')
                job = one(db, 'SELECT * FROM jobs WHERE id=?', (result['id'],))
                stamp = job['created_at']
                db.execute('INSERT INTO enquiry_intakes VALUES(?,?,?,?,?,?)', (job['id'], job['customer_id'], json.dumps(facts), operation_key, content_hash, stamp))
                for position, photo in enumerate(prepared):
                    photo_id, blob_id = uid(), uid()
                    write_photo(store, business_id, blob_id, photo['body'], created)
                    db.execute('INSERT INTO photo_blobs VALUES(?,?,?,?,?,?)', (blob_id, photo['sha256'], len(photo['body']), photo['width'], photo['height'], stamp))
                    db.execute('INSERT INTO job_photos(id,job_id,customer_id,blob_id,uploaded_by,membership_id,position,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)', (photo_id, job['id'], job['customer_id'], blob_id, PUBLIC_ACTOR, '', position, stamp, stamp))
                    db.execute('INSERT INTO enquiry_photo_sources VALUES(?,?,?,?,?,?,?,?)', (photo_id, job['id'], position, photo['filename'], photo['source_format'], photo['source_bytes'], photo['source_sha256'], stamp))
                    photo_snapshot(db, photo_id, PHOTO_ACTOR, 'photo.uploaded')
                if created:
                    descriptor = os.open(created[0].parent, os.O_RDONLY | os.O_DIRECTORY)
                    try: os.fsync(descriptor)
                    finally: os.close(descriptor)
                audit(db, 'enquiry.public_submitted', job['id'])
                return result
            result = once(db, request, fingerprint, action)
        return result
    except BaseException:
        # The connection has rolled back. Only files exclusively created by
        # this operation can be removed; no library-wide cleanup or staging URL.
        for path in created:
            path.unlink(missing_ok=True)
        raise


def with_intake(db, jobs):
    """Owner-only original submission facts; older jobs remain unknown."""
    for job in jobs:
        row = one(db, 'SELECT * FROM enquiry_intakes WHERE job_id=?', (job['id'],))
        job['enquiry_submission'] = None if not row else {
            'facts': json.loads(row['submitted']), 'created_at': row['created_at'],
            'original_customer_id': row['customer_id'],
            'photos': many(db, 'SELECT photo_id,position,filename,source_format FROM enquiry_photo_sources WHERE job_id=? ORDER BY position', (job['id'],))}
    return jobs
