#!/usr/bin/env python3
"""Review the bundled security lab through QDivZero. Python 3.11+, no packages."""
import argparse
from datetime import datetime, timezone
import json
import math
import os
from pathlib import Path
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener

MAX_RESPONSE_BYTES = 2 * 1024 * 1024
CASES = {
    'php-counter': ('php82_session_excerpt.c.txt', 'Diagnose this reproduced PHP 8.2.34 upload-progress regression using the selected original C excerpts. A benign multipart body is 262642 bytes: two files followed by another large form field. With session.upload_progress.cleanup=0, freq=1 and min_freq=0, the final session array has done=true but bytes_processed=136576. File byte counts are correct. Explain the concrete source-level operation that makes this final counter stale, citing original source lines. Propose a minimal unified diff to make the final counter equal the body length while preserving progress, cleanup and cancellation. State what the test establishes and what it cannot establish. Include the diagnosis, patch and regression expectation within the output limit.'),
    'php': ('php82_session_excerpt.c.txt', 'Review these selected C source excerpts from PHP 8.2.34. Identify a concrete defect supported by the code, cite the original file and line numbers, and trace the operations that cause it. Propose a minimal unified diff against this version that addresses the defect while preserving upload progress. Suggest a benign regression test with an explicit before/after result. Separate facts established by the excerpts from hypotheses requiring omitted code or runtime evidence. Keep the answer concise enough to include the diagnosis, patch and test within the output limit.'),
    'code': ('code_review.py.txt', 'Review this Python code for security problems. Cite the relevant lines, explain the impact, propose a parameterized fix and suggest a regression test. Distinguish the vulnerable function from the already parameterized function. Do not invent surrounding application behavior.'),
    'logs': ('ssh_events.log', 'Analyze these fictional SSH events. Give a timeline with timestamps and IP addresses, separate observed facts from hypotheses, and suggest the next evidence to check. Explain what the successful login does and does not prove. Do not infer country, reputation or CVEs from the IP addresses.'),
    'redteam': ('code_review.py.txt', 'Act as a red-team reviewer of this local SQLite lab. Propose a minimal SQL injection proof for the vulnerable function, explain how it changes the query and predict how many rows it returns when the table contains two customers. Then show the expected result for the same input with the parameterized function. Keep the demonstration in an in-memory SQLite database and explain the limits of the evidence.'),
}


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def case_prompt(case, language='es'):
    if case not in CASES or language not in ('es', 'en'):
        raise ValueError('Choose php, php-counter, code, logs or redteam, and es or en.')
    filename, task = CASES[case]
    fixture = (Path(__file__).resolve().parent / 'cases' / filename).read_text(encoding='utf-8')
    return json.dumps({
        'task': task,
        'answer_language': 'Spanish' if language == 'es' else 'English',
        'file': filename,
        'lines': [{'line': i, 'text': line} for i, line in enumerate(fixture.splitlines(), 1)],
    }, ensure_ascii=False, separators=(',', ':'))


def endpoint(base_url):
    try:
        parsed = urlsplit(base_url)
        if (any(c.isspace() for c in base_url) or not parsed.hostname
                or parsed.username is not None or parsed.password is not None
                or parsed.path not in ('', '/') or parsed.query or parsed.fragment
                or parsed.scheme not in ('https', 'http')):
            raise ValueError
        if parsed.scheme == 'http' and parsed.hostname not in ('localhost', '127.0.0.1', '::1'):
            raise ValueError
        parsed.port
        parsed.netloc.encode('ascii')
    except (ValueError, UnicodeError):
        raise ValueError('Use an HTTPS origin; plain HTTP is allowed only on loopback for offline tests.') from None
    return base_url.rstrip('/') + '/v1/chat/completions'


def review(case, output, *, base_url='https://api.qdiv0.com', timeout=180, language='es'):
    prompt = case_prompt(case, language)
    if not math.isfinite(timeout) or not 0 < timeout <= 600:
        raise ValueError('Timeout must be between 0 and 600 seconds.')
    credentials = {}
    for field in ('QDIV0_API_KEY', 'QDIV0_ACCOUNT_ID'):
        value = os.environ.get(field, '')
        if not value or any(ord(c) < 33 or ord(c) > 126 for c in value):
            raise ValueError(f'Set {field} to a nonempty, header-safe value.')
        credentials[field] = value
    model = os.environ.get('QDIV0_MODEL', 'ciberseguridad-qwen').strip()
    if not model or len(model) > 256 or any(ord(c) < 32 for c in model):
        raise ValueError('Set QDIV0_MODEL to the deployment serving name.')
    url = endpoint(base_url)
    payload = {
        'model': model,
        'messages': [{'role': 'user', 'content': prompt}],
        'temperature': 0.2,
        'max_tokens': 3072,
        'reasoning_effort': 'none',
        'stream': False,
    }
    request = Request(url, data=json.dumps(payload).encode('utf-8'), headers={
        'Authorization': 'Bearer ' + credentials['QDIV0_API_KEY'],
        'X-Account-Id': credentials['QDIV0_ACCOUNT_ID'],
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    }, method='POST')
    output = Path(output)
    output.parent.mkdir(parents=True, exist_ok=True)
    destination = output.open('x', encoding='utf-8')
    started_at = datetime.now(timezone.utc).isoformat()
    start = time.perf_counter()
    try:
        with destination:
            try:
                with build_opener(NoRedirect()).open(request, timeout=timeout) as response:
                    raw = response.read(MAX_RESPONSE_BYTES + 1)
            except HTTPError as error:
                status = error.code
                error.close()
                raise ValueError(f'HTTP {status}. Check deployment status before repeating the request.') from None
            except (URLError, TimeoutError):
                raise ValueError('Request failed or timed out. No automatic retry was sent; check the deployment before trying again.') from None
            elapsed = time.perf_counter() - start
            if len(raw) > MAX_RESPONSE_BYTES:
                raise ValueError('Response exceeded the 2 MiB limit.')
            try:
                body = json.loads(raw)
                choice = body['choices'][0]
                message = choice['message']
                content = message.get('content')
                reasoning = message.get('reasoning_content')
                if not any(isinstance(text, str) and text.strip() for text in (content, reasoning)):
                    raise ValueError
            except (ValueError, KeyError, IndexError, TypeError, AttributeError):
                raise ValueError('The server did not return a readable Chat Completions answer.') from None
            result = {
                'case': case, 'started_at': started_at, 'request_seconds': round(elapsed, 3),
                'truncated': choice.get('finish_reason') == 'length',
                'request': payload, 'response': body,
            }
            json.dump(result, destination, ensure_ascii=False, indent=2)
            destination.write('\n')
    except BaseException:
        destination.close()
        output.unlink(missing_ok=True)
        raise
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--case', choices=CASES, required=True)
    parser.add_argument('--output', type=Path, required=True, help='New JSON output file; existing files are preserved.')
    parser.add_argument('--language', choices=('es', 'en'), default='es')
    parser.add_argument('--timeout', type=float, default=180)
    args = parser.parse_args()
    try:
        result = review(args.case, args.output, timeout=args.timeout, language=args.language)
    except (ValueError, OSError) as error:
        parser.exit(1, f'{error}\n')
    message = result['response']['choices'][0]['message']
    print(message.get('content') or '[The server returned reasoning without a final answer; inspect the saved JSON.]')
    print(f'\nSaved {args.output} ({result["request_seconds"]:.3f} s).')
    if result['truncated']:
        print('The answer reached max_tokens and is incomplete. Inspect it before requesting another generation.')


if __name__ == '__main__':
    main()
